@runloop/rl-cli 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -29
- package/dist/cli.js +401 -92
- package/dist/commands/auth.js +12 -11
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +293 -225
- package/dist/commands/blueprint/logs.js +40 -0
- package/dist/commands/blueprint/preview.js +45 -0
- package/dist/commands/devbox/create.js +10 -9
- package/dist/commands/devbox/delete.js +8 -8
- package/dist/commands/devbox/download.js +49 -0
- package/dist/commands/devbox/exec.js +23 -13
- package/dist/commands/devbox/execAsync.js +43 -0
- package/dist/commands/devbox/get.js +37 -0
- package/dist/commands/devbox/getAsync.js +37 -0
- package/dist/commands/devbox/list.js +328 -190
- package/dist/commands/devbox/logs.js +40 -0
- package/dist/commands/devbox/read.js +49 -0
- package/dist/commands/devbox/resume.js +37 -0
- package/dist/commands/devbox/rsync.js +118 -0
- package/dist/commands/devbox/scp.js +122 -0
- package/dist/commands/devbox/shutdown.js +37 -0
- package/dist/commands/devbox/ssh.js +104 -0
- package/dist/commands/devbox/suspend.js +37 -0
- package/dist/commands/devbox/tunnel.js +120 -0
- package/dist/commands/devbox/upload.js +10 -10
- package/dist/commands/devbox/write.js +51 -0
- package/dist/commands/mcp-http.js +37 -0
- package/dist/commands/mcp-install.js +120 -0
- package/dist/commands/mcp.js +30 -0
- package/dist/commands/menu.js +20 -20
- package/dist/commands/object/delete.js +37 -0
- package/dist/commands/object/download.js +88 -0
- package/dist/commands/object/get.js +37 -0
- package/dist/commands/object/list.js +112 -0
- package/dist/commands/object/upload.js +130 -0
- package/dist/commands/snapshot/create.js +12 -11
- package/dist/commands/snapshot/delete.js +8 -8
- package/dist/commands/snapshot/list.js +56 -97
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +4 -4
- package/dist/components/Breadcrumb.js +55 -5
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +315 -178
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +180 -102
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +34 -33
- package/dist/components/MetadataDisplay.js +17 -9
- package/dist/components/OperationsMenu.js +6 -5
- package/dist/components/ResourceActionsMenu.js +117 -0
- package/dist/components/ResourceListView.js +213 -0
- package/dist/components/Spinner.js +5 -4
- package/dist/components/StatusBadge.js +81 -31
- package/dist/components/SuccessMessage.js +4 -3
- package/dist/components/Table.example.js +53 -23
- package/dist/components/Table.js +19 -11
- package/dist/hooks/useCursorPagination.js +125 -0
- package/dist/mcp/server-http.js +416 -0
- package/dist/mcp/server.js +397 -0
- package/dist/utils/CommandExecutor.js +16 -12
- package/dist/utils/client.js +7 -7
- package/dist/utils/config.js +130 -4
- package/dist/utils/interactiveCommand.js +2 -2
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +16 -12
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +4 -4
- package/package.json +29 -4
|
@@ -1,151 +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 {
|
|
17
|
-
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";
|
|
18
20
|
const PAGE_SIZE = 10;
|
|
19
21
|
const MAX_FETCH = 100;
|
|
20
|
-
// Format time ago
|
|
21
|
-
const formatTimeAgo = (timestamp) => {
|
|
22
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
23
|
-
if (seconds < 60)
|
|
24
|
-
return `${seconds}s ago`;
|
|
25
|
-
const minutes = Math.floor(seconds / 60);
|
|
26
|
-
if (minutes < 60)
|
|
27
|
-
return `${minutes}m ago`;
|
|
28
|
-
const hours = Math.floor(minutes / 60);
|
|
29
|
-
if (hours < 24)
|
|
30
|
-
return `${hours}h ago`;
|
|
31
|
-
const days = Math.floor(hours / 24);
|
|
32
|
-
if (days < 30)
|
|
33
|
-
return `${days}d ago`;
|
|
34
|
-
const months = Math.floor(days / 30);
|
|
35
|
-
if (months < 12)
|
|
36
|
-
return `${months}mo ago`;
|
|
37
|
-
const years = Math.floor(months / 12);
|
|
38
|
-
return `${years}y ago`;
|
|
39
|
-
};
|
|
40
22
|
const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
41
23
|
const { stdout } = useStdout();
|
|
42
|
-
const
|
|
43
|
-
const [loading, setLoading] = React.useState(true);
|
|
44
|
-
const [blueprints, setBlueprints] = React.useState([]);
|
|
45
|
-
const [error, setError] = React.useState(null);
|
|
46
|
-
const [currentPage, setCurrentPage] = React.useState(0);
|
|
47
|
-
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
48
|
-
const [showDetails, setShowDetails] = React.useState(false);
|
|
24
|
+
const [selectedBlueprint, setSelectedBlueprint] = React.useState(null);
|
|
49
25
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
50
26
|
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
51
|
-
const [operationInput, setOperationInput] = React.useState(
|
|
27
|
+
const [operationInput, setOperationInput] = React.useState("");
|
|
52
28
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
53
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);
|
|
54
39
|
// Calculate responsive column widths
|
|
55
40
|
const terminalWidth = stdout?.columns || 120;
|
|
56
41
|
const showDescription = terminalWidth >= 120;
|
|
57
|
-
const
|
|
42
|
+
const statusIconWidth = 2;
|
|
43
|
+
const statusTextWidth = 10;
|
|
58
44
|
const idWidth = 25;
|
|
59
45
|
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
60
46
|
const descriptionWidth = 40;
|
|
61
47
|
const timeWidth = 20;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
76
67
|
icon: figures.cross,
|
|
77
|
-
}
|
|
78
|
-
|
|
68
|
+
});
|
|
69
|
+
return operations;
|
|
70
|
+
};
|
|
71
|
+
// Fetch blueprints - moved to top to ensure hooks are called in same order
|
|
79
72
|
React.useEffect(() => {
|
|
80
|
-
const
|
|
73
|
+
const fetchBlueprints = async () => {
|
|
81
74
|
try {
|
|
75
|
+
setLoading(true);
|
|
82
76
|
const client = getClient();
|
|
83
77
|
const allBlueprints = [];
|
|
84
78
|
let count = 0;
|
|
85
79
|
for await (const blueprint of client.blueprints.list()) {
|
|
86
80
|
allBlueprints.push(blueprint);
|
|
87
81
|
count++;
|
|
88
|
-
if (count >= MAX_FETCH)
|
|
82
|
+
if (count >= MAX_FETCH)
|
|
89
83
|
break;
|
|
90
|
-
}
|
|
91
84
|
}
|
|
92
85
|
setBlueprints(allBlueprints);
|
|
93
86
|
}
|
|
94
87
|
catch (err) {
|
|
95
|
-
|
|
88
|
+
setListError(err);
|
|
96
89
|
}
|
|
97
90
|
finally {
|
|
98
91
|
setLoading(false);
|
|
99
92
|
}
|
|
100
93
|
};
|
|
101
|
-
|
|
94
|
+
fetchBlueprints();
|
|
102
95
|
}, []);
|
|
103
|
-
//
|
|
104
|
-
const prevShowDetailsRef = React.useRef(showDetails);
|
|
105
|
-
React.useEffect(() => {
|
|
106
|
-
if (showDetails && !prevShowDetailsRef.current) {
|
|
107
|
-
console.clear();
|
|
108
|
-
}
|
|
109
|
-
prevShowDetailsRef.current = showDetails;
|
|
110
|
-
}, [showDetails]);
|
|
111
|
-
// Auto-execute operations that don't need input
|
|
112
|
-
React.useEffect(() => {
|
|
113
|
-
if (executingOperation === 'delete' && !loading && selectedBlueprint) {
|
|
114
|
-
executeOperation();
|
|
115
|
-
}
|
|
116
|
-
}, [executingOperation]);
|
|
117
|
-
const executeOperation = async () => {
|
|
118
|
-
const client = getClient();
|
|
119
|
-
const blueprint = selectedBlueprint;
|
|
120
|
-
try {
|
|
121
|
-
setLoading(true);
|
|
122
|
-
switch (executingOperation) {
|
|
123
|
-
case 'create_devbox':
|
|
124
|
-
const devbox = await client.devboxes.create({
|
|
125
|
-
blueprint_id: blueprint.id,
|
|
126
|
-
name: operationInput || undefined,
|
|
127
|
-
});
|
|
128
|
-
setOperationResult(`Devbox created successfully!\n\n` +
|
|
129
|
-
`ID: ${devbox.id}\n` +
|
|
130
|
-
`Name: ${devbox.name || '(none)'}\n` +
|
|
131
|
-
`Status: ${devbox.status}\n\n` +
|
|
132
|
-
`Use 'rln devbox list' to view all devboxes.`);
|
|
133
|
-
break;
|
|
134
|
-
case 'delete':
|
|
135
|
-
await client.blueprints.delete(blueprint.id);
|
|
136
|
-
setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
setOperationError(err);
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
setLoading(false);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
96
|
+
// Handle input for all views - combined into single hook
|
|
147
97
|
useInput((input, key) => {
|
|
148
|
-
|
|
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
|
+
}
|
|
149
103
|
// Handle operation input mode
|
|
150
104
|
if (executingOperation && !operationResult && !operationError) {
|
|
151
105
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
@@ -153,95 +107,130 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
153
107
|
if (key.return) {
|
|
154
108
|
executeOperation();
|
|
155
109
|
}
|
|
156
|
-
else if (input ===
|
|
110
|
+
else if (input === "q" || key.escape) {
|
|
157
111
|
console.clear();
|
|
158
112
|
setExecutingOperation(null);
|
|
159
|
-
setOperationInput(
|
|
113
|
+
setOperationInput("");
|
|
160
114
|
}
|
|
161
115
|
}
|
|
162
116
|
return;
|
|
163
117
|
}
|
|
164
118
|
// Handle operation result display
|
|
165
119
|
if (operationResult || operationError) {
|
|
166
|
-
if (input ===
|
|
120
|
+
if (input === "q" || key.escape || key.return) {
|
|
167
121
|
console.clear();
|
|
168
122
|
setOperationResult(null);
|
|
169
123
|
setOperationError(null);
|
|
170
124
|
setExecutingOperation(null);
|
|
171
|
-
setOperationInput(
|
|
125
|
+
setOperationInput("");
|
|
172
126
|
}
|
|
173
127
|
return;
|
|
174
128
|
}
|
|
175
|
-
// Handle
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
else if (input === 'o' && selectedBlueprint) {
|
|
183
|
-
// Open in browser
|
|
184
|
-
const url = getBlueprintUrl(selectedBlueprint.id);
|
|
185
|
-
const openBrowser = async () => {
|
|
186
|
-
const { exec } = await import('child_process');
|
|
187
|
-
const platform = process.platform;
|
|
188
|
-
let openCommand;
|
|
189
|
-
if (platform === 'darwin') {
|
|
190
|
-
openCommand = `open "${url}"`;
|
|
191
|
-
}
|
|
192
|
-
else if (platform === 'win32') {
|
|
193
|
-
openCommand = `start "${url}"`;
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
openCommand = `xdg-open "${url}"`;
|
|
197
|
-
}
|
|
198
|
-
exec(openCommand);
|
|
199
|
-
};
|
|
200
|
-
openBrowser();
|
|
201
|
-
}
|
|
202
|
-
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) {
|
|
203
136
|
setSelectedOperation(selectedOperation - 1);
|
|
204
137
|
}
|
|
205
|
-
else if (key.downArrow &&
|
|
138
|
+
else if (key.downArrow &&
|
|
139
|
+
selectedOperation < allOperations.length - 1) {
|
|
206
140
|
setSelectedOperation(selectedOperation + 1);
|
|
207
141
|
}
|
|
208
142
|
else if (key.return) {
|
|
209
143
|
console.clear();
|
|
210
|
-
|
|
211
|
-
|
|
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);
|
|
212
193
|
}
|
|
213
194
|
return;
|
|
214
195
|
}
|
|
215
|
-
// 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;
|
|
216
203
|
if (key.upArrow && selectedIndex > 0) {
|
|
217
204
|
setSelectedIndex(selectedIndex - 1);
|
|
218
205
|
}
|
|
219
206
|
else if (key.downArrow && selectedIndex < pageBlueprints - 1) {
|
|
220
207
|
setSelectedIndex(selectedIndex + 1);
|
|
221
208
|
}
|
|
222
|
-
else if ((input ===
|
|
209
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
210
|
+
currentPage < totalPages - 1) {
|
|
223
211
|
setCurrentPage(currentPage + 1);
|
|
224
212
|
setSelectedIndex(0);
|
|
225
213
|
}
|
|
226
|
-
else if ((input ===
|
|
214
|
+
else if ((input === "p" || key.leftArrow) && currentPage > 0) {
|
|
227
215
|
setCurrentPage(currentPage - 1);
|
|
228
216
|
setSelectedIndex(0);
|
|
229
217
|
}
|
|
230
|
-
else if (
|
|
218
|
+
else if (input === "a") {
|
|
231
219
|
console.clear();
|
|
232
|
-
|
|
220
|
+
setShowPopup(true);
|
|
221
|
+
setSelectedOperation(0);
|
|
233
222
|
}
|
|
234
|
-
else if (input ===
|
|
223
|
+
else if (input === "o" && currentBlueprints[selectedIndex]) {
|
|
235
224
|
// Open in browser
|
|
236
|
-
const url = getBlueprintUrl(
|
|
225
|
+
const url = getBlueprintUrl(currentBlueprints[selectedIndex].id);
|
|
237
226
|
const openBrowser = async () => {
|
|
238
|
-
const { exec } = await import(
|
|
227
|
+
const { exec } = await import("child_process");
|
|
239
228
|
const platform = process.platform;
|
|
240
229
|
let openCommand;
|
|
241
|
-
if (platform ===
|
|
230
|
+
if (platform === "darwin") {
|
|
242
231
|
openCommand = `open "${url}"`;
|
|
243
232
|
}
|
|
244
|
-
else if (platform ===
|
|
233
|
+
else if (platform === "win32") {
|
|
245
234
|
openCommand = `start "${url}"`;
|
|
246
235
|
}
|
|
247
236
|
else {
|
|
@@ -258,26 +247,58 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
258
247
|
else if (onExit) {
|
|
259
248
|
onExit();
|
|
260
249
|
}
|
|
261
|
-
else {
|
|
262
|
-
inkExit();
|
|
263
|
-
}
|
|
264
250
|
}
|
|
265
251
|
});
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
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);
|
|
269
257
|
const currentBlueprints = blueprints.slice(startIndex, endIndex);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
};
|
|
274
295
|
// Filter operations based on blueprint status
|
|
275
296
|
const operations = selectedBlueprint
|
|
276
297
|
? allOperations.filter((op) => {
|
|
277
298
|
const status = selectedBlueprint.status;
|
|
278
299
|
// Only allow creating devbox if build is complete
|
|
279
|
-
if (op.key ===
|
|
280
|
-
return status ===
|
|
300
|
+
if (op.key === "create_devbox") {
|
|
301
|
+
return status === "build_complete";
|
|
281
302
|
}
|
|
282
303
|
// Allow delete for any status
|
|
283
304
|
return true;
|
|
@@ -285,91 +306,138 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
|
285
306
|
: allOperations;
|
|
286
307
|
// Operation result display
|
|
287
308
|
if (operationResult || operationError) {
|
|
288
|
-
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
309
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
310
|
+
"Operation";
|
|
289
311
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
290
|
-
{ label:
|
|
312
|
+
{ label: "Blueprints" },
|
|
291
313
|
{
|
|
292
|
-
label: selectedBlueprint?.name || selectedBlueprint?.id ||
|
|
314
|
+
label: selectedBlueprint?.name || selectedBlueprint?.id || "Blueprint",
|
|
293
315
|
},
|
|
294
316
|
{ label: operationLabel, active: true },
|
|
295
|
-
] }), _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" }) })] }));
|
|
296
318
|
}
|
|
297
319
|
// Operation input mode
|
|
298
320
|
if (executingOperation && selectedBlueprint) {
|
|
299
321
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
300
322
|
const needsInput = currentOp?.needsInput;
|
|
301
|
-
const operationLabel = currentOp?.label ||
|
|
323
|
+
const operationLabel = currentOp?.label || "Operation";
|
|
302
324
|
if (loading) {
|
|
303
325
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
304
|
-
{ label:
|
|
326
|
+
{ label: "Blueprints" },
|
|
305
327
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
306
328
|
{ label: operationLabel, active: true },
|
|
307
329
|
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
308
330
|
}
|
|
309
331
|
if (!needsInput) {
|
|
310
332
|
const messages = {
|
|
311
|
-
delete:
|
|
333
|
+
delete: "Deleting blueprint...",
|
|
312
334
|
};
|
|
313
335
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
314
|
-
{ label:
|
|
336
|
+
{ label: "Blueprints" },
|
|
315
337
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
316
338
|
{ label: operationLabel, active: true },
|
|
317
|
-
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] ||
|
|
339
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
318
340
|
}
|
|
319
341
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
320
|
-
{ label:
|
|
342
|
+
{ label: "Blueprints" },
|
|
321
343
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
322
344
|
{ label: operationLabel, active: true },
|
|
323
|
-
] }), _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" }) })] })] }));
|
|
324
346
|
}
|
|
325
|
-
//
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (direction === 'up' && selectedOperation > 0) {
|
|
340
|
-
setSelectedOperation(selectedOperation - 1);
|
|
341
|
-
}
|
|
342
|
-
else if (direction === 'down' && selectedOperation < operations.length - 1) {
|
|
343
|
-
setSelectedOperation(selectedOperation + 1);
|
|
344
|
-
}
|
|
345
|
-
}, onSelect: (op) => {
|
|
346
|
-
console.clear();
|
|
347
|
-
setExecutingOperation(op.key);
|
|
348
|
-
}, onBack: () => {
|
|
349
|
-
console.clear();
|
|
350
|
-
setShowDetails(false);
|
|
351
|
-
setSelectedOperation(0);
|
|
352
|
-
}, 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 }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
|
|
353
361
|
}
|
|
362
|
+
// Error state
|
|
363
|
+
if (listError) {
|
|
364
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }], showVersionCheck: 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 }], showVersionCheck: 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)
|
|
354
372
|
// List view
|
|
355
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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"] })] })] }));
|
|
373
441
|
};
|
|
374
442
|
// Export the UI component for use in the main menu
|
|
375
443
|
export { ListBlueprintsUI };
|