@runloop/rl-cli 0.0.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/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/cli.js +110 -0
- package/dist/commands/auth.js +27 -0
- package/dist/commands/blueprint/list.js +373 -0
- package/dist/commands/create.js +42 -0
- package/dist/commands/delete.js +34 -0
- package/dist/commands/devbox/create.js +45 -0
- package/dist/commands/devbox/delete.js +37 -0
- package/dist/commands/devbox/exec.js +35 -0
- package/dist/commands/devbox/list.js +302 -0
- package/dist/commands/devbox/upload.js +40 -0
- package/dist/commands/exec.js +35 -0
- package/dist/commands/list.js +59 -0
- package/dist/commands/snapshot/create.js +40 -0
- package/dist/commands/snapshot/delete.js +37 -0
- package/dist/commands/snapshot/list.js +122 -0
- package/dist/commands/upload.js +40 -0
- package/dist/components/Banner.js +11 -0
- package/dist/components/Breadcrumb.js +9 -0
- package/dist/components/DetailView.js +29 -0
- package/dist/components/DevboxCard.js +24 -0
- package/dist/components/DevboxCreatePage.js +370 -0
- package/dist/components/DevboxDetailPage.js +621 -0
- package/dist/components/ErrorMessage.js +6 -0
- package/dist/components/Header.js +6 -0
- package/dist/components/MetadataDisplay.js +24 -0
- package/dist/components/OperationsMenu.js +19 -0
- package/dist/components/Spinner.js +6 -0
- package/dist/components/StatusBadge.js +41 -0
- package/dist/components/SuccessMessage.js +6 -0
- package/dist/components/Table.example.js +55 -0
- package/dist/components/Table.js +54 -0
- package/dist/utils/CommandExecutor.js +115 -0
- package/dist/utils/client.js +13 -0
- package/dist/utils/config.js +17 -0
- package/dist/utils/output.js +115 -0
- package/package.json +69 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text, useInput, useStdout, useApp } 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 { StatusBadge } from '../../components/StatusBadge.js';
|
|
12
|
+
import { Breadcrumb } from '../../components/Breadcrumb.js';
|
|
13
|
+
import { MetadataDisplay } from '../../components/MetadataDisplay.js';
|
|
14
|
+
import { Table, createTextColumn, createComponentColumn } from '../../components/Table.js';
|
|
15
|
+
import { OperationsMenu } from '../../components/OperationsMenu.js';
|
|
16
|
+
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
17
|
+
const PAGE_SIZE = 10;
|
|
18
|
+
const MAX_FETCH = 100;
|
|
19
|
+
// Format time ago
|
|
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 = () => {
|
|
40
|
+
const { stdout } = useStdout();
|
|
41
|
+
const { exit } = useApp();
|
|
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);
|
|
48
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
49
|
+
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
50
|
+
const [operationInput, setOperationInput] = React.useState('');
|
|
51
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
52
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
53
|
+
// Calculate responsive column widths
|
|
54
|
+
const terminalWidth = stdout?.columns || 120;
|
|
55
|
+
const showDescription = terminalWidth >= 120;
|
|
56
|
+
const showFullId = terminalWidth >= 80;
|
|
57
|
+
const idWidth = 25;
|
|
58
|
+
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
59
|
+
const descriptionWidth = 40;
|
|
60
|
+
const timeWidth = 20;
|
|
61
|
+
const allOperations = [
|
|
62
|
+
{
|
|
63
|
+
key: 'create_devbox',
|
|
64
|
+
label: 'Create Devbox from Blueprint',
|
|
65
|
+
color: 'green',
|
|
66
|
+
icon: figures.play,
|
|
67
|
+
needsInput: true,
|
|
68
|
+
inputPrompt: 'Devbox name (optional):',
|
|
69
|
+
inputPlaceholder: 'my-devbox',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'delete',
|
|
73
|
+
label: 'Delete Blueprint',
|
|
74
|
+
color: 'red',
|
|
75
|
+
icon: figures.cross,
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
const list = async () => {
|
|
80
|
+
try {
|
|
81
|
+
const client = getClient();
|
|
82
|
+
const allBlueprints = [];
|
|
83
|
+
let count = 0;
|
|
84
|
+
for await (const blueprint of client.blueprints.list()) {
|
|
85
|
+
allBlueprints.push(blueprint);
|
|
86
|
+
count++;
|
|
87
|
+
if (count >= MAX_FETCH) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
setBlueprints(allBlueprints);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
setError(err);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
setLoading(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
list();
|
|
101
|
+
}, []);
|
|
102
|
+
// Clear console when transitioning to detail view
|
|
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
|
+
};
|
|
146
|
+
useInput((input, key) => {
|
|
147
|
+
const pageBlueprints = currentBlueprints.length;
|
|
148
|
+
// Handle operation input mode
|
|
149
|
+
if (executingOperation && !operationResult && !operationError) {
|
|
150
|
+
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
151
|
+
if (currentOp?.needsInput) {
|
|
152
|
+
if (key.return) {
|
|
153
|
+
executeOperation();
|
|
154
|
+
}
|
|
155
|
+
else if (input === 'q' || key.escape) {
|
|
156
|
+
console.clear();
|
|
157
|
+
setExecutingOperation(null);
|
|
158
|
+
setOperationInput('');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Handle operation result display
|
|
164
|
+
if (operationResult || operationError) {
|
|
165
|
+
if (input === 'q' || key.escape || key.return) {
|
|
166
|
+
console.clear();
|
|
167
|
+
setOperationResult(null);
|
|
168
|
+
setOperationError(null);
|
|
169
|
+
setExecutingOperation(null);
|
|
170
|
+
setOperationInput('');
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Handle details view
|
|
175
|
+
if (showDetails) {
|
|
176
|
+
if (input === 'q' || key.escape) {
|
|
177
|
+
console.clear();
|
|
178
|
+
setShowDetails(false);
|
|
179
|
+
setSelectedOperation(0);
|
|
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) {
|
|
202
|
+
setSelectedOperation(selectedOperation - 1);
|
|
203
|
+
}
|
|
204
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
205
|
+
setSelectedOperation(selectedOperation + 1);
|
|
206
|
+
}
|
|
207
|
+
else if (key.return) {
|
|
208
|
+
console.clear();
|
|
209
|
+
const op = operations[selectedOperation].key;
|
|
210
|
+
setExecutingOperation(op);
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Handle list view
|
|
215
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
216
|
+
setSelectedIndex(selectedIndex - 1);
|
|
217
|
+
}
|
|
218
|
+
else if (key.downArrow && selectedIndex < pageBlueprints - 1) {
|
|
219
|
+
setSelectedIndex(selectedIndex + 1);
|
|
220
|
+
}
|
|
221
|
+
else if ((input === 'n' || key.rightArrow) && currentPage < totalPages - 1) {
|
|
222
|
+
setCurrentPage(currentPage + 1);
|
|
223
|
+
setSelectedIndex(0);
|
|
224
|
+
}
|
|
225
|
+
else if ((input === 'p' || key.leftArrow) && currentPage > 0) {
|
|
226
|
+
setCurrentPage(currentPage - 1);
|
|
227
|
+
setSelectedIndex(0);
|
|
228
|
+
}
|
|
229
|
+
else if (key.return) {
|
|
230
|
+
console.clear();
|
|
231
|
+
setShowDetails(true);
|
|
232
|
+
}
|
|
233
|
+
else if (input === 'o' && selectedBlueprint) {
|
|
234
|
+
// Open in browser
|
|
235
|
+
const url = `https://platform.runloop.ai/blueprints/${selectedBlueprint.id}`;
|
|
236
|
+
const openBrowser = async () => {
|
|
237
|
+
const { exec } = await import('child_process');
|
|
238
|
+
const platform = process.platform;
|
|
239
|
+
let openCommand;
|
|
240
|
+
if (platform === 'darwin') {
|
|
241
|
+
openCommand = `open "${url}"`;
|
|
242
|
+
}
|
|
243
|
+
else if (platform === 'win32') {
|
|
244
|
+
openCommand = `start "${url}"`;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
openCommand = `xdg-open "${url}"`;
|
|
248
|
+
}
|
|
249
|
+
exec(openCommand);
|
|
250
|
+
};
|
|
251
|
+
openBrowser();
|
|
252
|
+
}
|
|
253
|
+
else if (input === 'q') {
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const totalPages = Math.ceil(blueprints.length / PAGE_SIZE);
|
|
258
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
259
|
+
const endIndex = Math.min(startIndex + PAGE_SIZE, blueprints.length);
|
|
260
|
+
const currentBlueprints = blueprints.slice(startIndex, endIndex);
|
|
261
|
+
const selectedBlueprint = currentBlueprints[selectedIndex];
|
|
262
|
+
const buildComplete = blueprints.filter((b) => b.status === 'build_complete').length;
|
|
263
|
+
const building = blueprints.filter((b) => ['provisioning', 'building'].includes(b.status)).length;
|
|
264
|
+
const failed = blueprints.filter((b) => b.status === 'build_failed').length;
|
|
265
|
+
// Filter operations based on blueprint status
|
|
266
|
+
const operations = selectedBlueprint
|
|
267
|
+
? allOperations.filter((op) => {
|
|
268
|
+
const status = selectedBlueprint.status;
|
|
269
|
+
// Only allow creating devbox if build is complete
|
|
270
|
+
if (op.key === 'create_devbox') {
|
|
271
|
+
return status === 'build_complete';
|
|
272
|
+
}
|
|
273
|
+
// Allow delete for any status
|
|
274
|
+
return true;
|
|
275
|
+
})
|
|
276
|
+
: allOperations;
|
|
277
|
+
// Operation result display
|
|
278
|
+
if (operationResult || operationError) {
|
|
279
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
|
|
280
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
281
|
+
{ label: 'Blueprints' },
|
|
282
|
+
{
|
|
283
|
+
label: selectedBlueprint?.name || selectedBlueprint?.id || 'Blueprint',
|
|
284
|
+
},
|
|
285
|
+
{ 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: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
287
|
+
}
|
|
288
|
+
// Operation input mode
|
|
289
|
+
if (executingOperation && selectedBlueprint) {
|
|
290
|
+
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
291
|
+
const needsInput = currentOp?.needsInput;
|
|
292
|
+
const operationLabel = currentOp?.label || 'Operation';
|
|
293
|
+
if (loading) {
|
|
294
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
295
|
+
{ label: 'Blueprints' },
|
|
296
|
+
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
297
|
+
{ label: operationLabel, active: true },
|
|
298
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
299
|
+
}
|
|
300
|
+
if (!needsInput) {
|
|
301
|
+
const messages = {
|
|
302
|
+
delete: 'Deleting blueprint...',
|
|
303
|
+
};
|
|
304
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
305
|
+
{ label: 'Blueprints' },
|
|
306
|
+
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
307
|
+
{ label: operationLabel, active: true },
|
|
308
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
|
|
309
|
+
}
|
|
310
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
311
|
+
{ label: 'Blueprints' },
|
|
312
|
+
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
313
|
+
{ label: operationLabel, active: true },
|
|
314
|
+
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", 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: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
315
|
+
}
|
|
316
|
+
// Details view with operation selection
|
|
317
|
+
if (showDetails && selectedBlueprint) {
|
|
318
|
+
const ds = selectedBlueprint.dockerfile_setup;
|
|
319
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
320
|
+
{ label: 'Blueprints' },
|
|
321
|
+
{
|
|
322
|
+
label: selectedBlueprint.name || selectedBlueprint.id,
|
|
323
|
+
active: true,
|
|
324
|
+
},
|
|
325
|
+
] }), _jsx(Header, { title: "Blueprint Details" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: selectedBlueprint.name || selectedBlueprint.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedBlueprint.status }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "\u2022 ", selectedBlueprint.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: selectedBlueprint.create_time_ms
|
|
326
|
+
? new Date(selectedBlueprint.create_time_ms).toLocaleString()
|
|
327
|
+
: '' }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "(", selectedBlueprint.create_time_ms
|
|
328
|
+
? formatTimeAgo(selectedBlueprint.create_time_ms)
|
|
329
|
+
: '', ")"] })] }), ds?.description && (_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: ds.description }) }))] }), ds && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "yellow", bold: true, children: [figures.squareSmallFilled, " Dockerfile Setup"] }), ds.base_image && (_jsxs(Text, { dimColor: true, children: ["Base Image: ", ds.base_image] })), ds.entrypoint && (_jsxs(Text, { dimColor: true, children: ["Entrypoint: ", ds.entrypoint] })), ds.system_packages && ds.system_packages.length > 0 && (_jsxs(Text, { dimColor: true, children: ["System Packages: ", ds.system_packages.join(', ')] })), ds.python_packages && ds.python_packages.length > 0 && (_jsxs(Text, { dimColor: true, children: ["Python Packages: ", ds.python_packages.join(', ')] }))] })), selectedBlueprint.metadata && Object.keys(selectedBlueprint.metadata).length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, children: _jsx(MetadataDisplay, { metadata: selectedBlueprint.metadata, showBorder: false }) })), selectedBlueprint.build_error && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, ' '] }), _jsx(Text, { color: "red", dimColor: true, children: selectedBlueprint.build_error })] })), _jsx(OperationsMenu, { operations: operations, selectedIndex: selectedOperation, onNavigate: (direction) => {
|
|
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: () => { } }] })] }));
|
|
344
|
+
}
|
|
345
|
+
// List view
|
|
346
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: 'Blueprints', active: true }] }), loading && _jsx(SpinnerComponent, { message: "Loading blueprints..." }), !loading && !error && blueprints.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No blueprints found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln blueprint create" })] })), !loading && !error && blueprints.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", buildComplete] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", building] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "red", children: [figures.cross, " ", failed] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", blueprints.length, blueprints.length >= MAX_FETCH && '+'] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Page ", currentPage + 1, "/", totalPages] })] }))] }), _jsx(Table, { data: currentBlueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, columns: [
|
|
347
|
+
createComponentColumn('status', 'Status', (blueprint) => _jsx(StatusBadge, { status: blueprint.status, showText: false }), { width: 2 }),
|
|
348
|
+
createTextColumn('id', 'ID', (blueprint) => (showFullId ? blueprint.id : blueprint.id.slice(0, 13)), {
|
|
349
|
+
width: showFullId ? idWidth : 15,
|
|
350
|
+
color: 'gray',
|
|
351
|
+
dimColor: true,
|
|
352
|
+
bold: false,
|
|
353
|
+
}),
|
|
354
|
+
createTextColumn('name', 'Name', (blueprint) => blueprint.name || '(unnamed)', { width: nameWidth }),
|
|
355
|
+
createTextColumn('description', 'Description', (blueprint) => blueprint.dockerfile_setup?.description || '', {
|
|
356
|
+
width: descriptionWidth,
|
|
357
|
+
color: 'gray',
|
|
358
|
+
dimColor: true,
|
|
359
|
+
bold: false,
|
|
360
|
+
visible: showDescription,
|
|
361
|
+
}),
|
|
362
|
+
createTextColumn('created', 'Created', (blueprint) => blueprint.create_time_ms ? formatTimeAgo(blueprint.create_time_ms) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
|
|
363
|
+
] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Operations \u2022 [o] Open in Browser \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[q] Quit"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list blueprints", error: error })] }));
|
|
364
|
+
};
|
|
365
|
+
export async function listBlueprints(options = {}) {
|
|
366
|
+
const executor = createExecutor(options);
|
|
367
|
+
await executor.executeList(async () => {
|
|
368
|
+
const client = executor.getClient();
|
|
369
|
+
return executor.fetchFromIterator(client.blueprints.list(), {
|
|
370
|
+
limit: PAGE_SIZE,
|
|
371
|
+
});
|
|
372
|
+
}, () => _jsx(ListBlueprintsUI, {}), PAGE_SIZE);
|
|
373
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, Box, Text } from 'ink';
|
|
4
|
+
import Gradient from 'ink-gradient';
|
|
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 { SuccessMessage } from '../components/SuccessMessage.js';
|
|
10
|
+
import { ErrorMessage } from '../components/ErrorMessage.js';
|
|
11
|
+
const CreateDevboxUI = ({ name, template }) => {
|
|
12
|
+
const [loading, setLoading] = React.useState(true);
|
|
13
|
+
const [result, setResult] = React.useState(null);
|
|
14
|
+
const [error, setError] = React.useState(null);
|
|
15
|
+
const [progress, setProgress] = React.useState('Initializing...');
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
const create = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const client = getClient();
|
|
20
|
+
setProgress('Requesting new devbox...');
|
|
21
|
+
const devbox = await client.devboxes.create({
|
|
22
|
+
name: name || `devbox-${Date.now()}`,
|
|
23
|
+
...(template && { template }),
|
|
24
|
+
});
|
|
25
|
+
setProgress('Devbox created! Waiting for provisioning...');
|
|
26
|
+
setResult(devbox);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
setError(err);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
create();
|
|
36
|
+
}, []);
|
|
37
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Create Devbox", subtitle: "Setting up your new development environment" }), loading && (_jsxs(_Fragment, { children: [_jsx(SpinnerComponent, { message: progress }), _jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "cyan", bold: true, children: [figures.info, " Configuration"] }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.pointer, " Name: "] }), _jsx(Text, { color: "white", children: name || '(auto-generated)' })] }), template && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.pointer, " Template: "] }), _jsx(Text, { color: "white", children: template })] }))] })] })] })), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Devbox created successfully!", details: `ID: ${result.id}\nName: ${result.name || '(unnamed)'}\nStatus: ${result.status}` }), _jsxs(Box, { borderStyle: "double", borderColor: "green", paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Gradient, { name: "summer", children: _jsxs(Text, { bold: true, children: [figures.star, " Next Steps"] }) }) }), _jsxs(Box, { flexDirection: "column", gap: 1, marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.tick, " Execute commands: "] }), _jsxs(Text, { color: "cyan", children: ["rln exec ", result.id.slice(0, 8), "... <command>"] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.tick, " Upload files: "] }), _jsxs(Text, { color: "cyan", children: ["rln upload ", result.id.slice(0, 8), "... <file>"] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.tick, " View all: "] }), _jsx(Text, { color: "cyan", children: "rln list" })] })] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create devbox", error: error })] }));
|
|
38
|
+
};
|
|
39
|
+
export async function createDevbox(options) {
|
|
40
|
+
const { waitUntilExit } = render(_jsx(CreateDevboxUI, { name: options.name, template: options.template }));
|
|
41
|
+
await waitUntilExit();
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { getClient } from '../utils/client.js';
|
|
5
|
+
import { Header } from '../components/Header.js';
|
|
6
|
+
import { SpinnerComponent } from '../components/Spinner.js';
|
|
7
|
+
import { SuccessMessage } from '../components/SuccessMessage.js';
|
|
8
|
+
import { ErrorMessage } from '../components/ErrorMessage.js';
|
|
9
|
+
const DeleteDevboxUI = ({ id }) => {
|
|
10
|
+
const [loading, setLoading] = React.useState(true);
|
|
11
|
+
const [success, setSuccess] = React.useState(false);
|
|
12
|
+
const [error, setError] = React.useState(null);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const deleteDevbox = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const client = getClient();
|
|
17
|
+
await client.devboxes.shutdown(id);
|
|
18
|
+
setSuccess(true);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
setError(err);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
deleteDevbox();
|
|
28
|
+
}, []);
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Delete Devbox", subtitle: `Deleting devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Deleting devbox..." }), success && (_jsx(SuccessMessage, { message: "Devbox deleted successfully!", details: `ID: ${id}` })), error && _jsx(ErrorMessage, { message: "Failed to delete devbox", error: error })] }));
|
|
30
|
+
};
|
|
31
|
+
export async function deleteDevbox(id) {
|
|
32
|
+
const { waitUntilExit } = render(_jsx(DeleteDevboxUI, { id: id }));
|
|
33
|
+
await waitUntilExit();
|
|
34
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { getClient } from '../../utils/client.js';
|
|
5
|
+
import { Header } from '../../components/Header.js';
|
|
6
|
+
import { Banner } from '../../components/Banner.js';
|
|
7
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
8
|
+
import { SuccessMessage } from '../../components/SuccessMessage.js';
|
|
9
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
10
|
+
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
11
|
+
const CreateDevboxUI = ({ name, template }) => {
|
|
12
|
+
const [loading, setLoading] = React.useState(true);
|
|
13
|
+
const [result, setResult] = React.useState(null);
|
|
14
|
+
const [error, setError] = React.useState(null);
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
const create = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const client = getClient();
|
|
19
|
+
const devbox = await client.devboxes.create({
|
|
20
|
+
name: name || `devbox-${Date.now()}`,
|
|
21
|
+
...(template && { template }),
|
|
22
|
+
});
|
|
23
|
+
setResult(devbox);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
setError(err);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
create();
|
|
33
|
+
}, []);
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Header, { title: "Create Devbox" }), loading && _jsx(SpinnerComponent, { message: "Creating..." }), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Devbox created!", details: `ID: ${result.id}\nStatus: ${result.status}` }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Try: " }), _jsxs(Text, { color: "cyan", children: ["rln devbox exec ", result.id, " ls"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create devbox", error: error })] }));
|
|
35
|
+
};
|
|
36
|
+
export async function createDevbox(options) {
|
|
37
|
+
const executor = createExecutor(options);
|
|
38
|
+
await executor.executeAction(async () => {
|
|
39
|
+
const client = executor.getClient();
|
|
40
|
+
return client.devboxes.create({
|
|
41
|
+
name: options.name || `devbox-${Date.now()}`,
|
|
42
|
+
...(options.template && { template: options.template }),
|
|
43
|
+
});
|
|
44
|
+
}, () => _jsx(CreateDevboxUI, { name: options.name, template: options.template }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { getClient } from '../../utils/client.js';
|
|
4
|
+
import { Header } from '../../components/Header.js';
|
|
5
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
6
|
+
import { SuccessMessage } from '../../components/SuccessMessage.js';
|
|
7
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
8
|
+
import { createExecutor } from '../../utils/CommandExecutor.js';
|
|
9
|
+
const DeleteDevboxUI = ({ id }) => {
|
|
10
|
+
const [loading, setLoading] = React.useState(true);
|
|
11
|
+
const [success, setSuccess] = React.useState(false);
|
|
12
|
+
const [error, setError] = React.useState(null);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const deleteDevbox = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const client = getClient();
|
|
17
|
+
await client.devboxes.shutdown(id);
|
|
18
|
+
setSuccess(true);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
setError(err);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setLoading(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
deleteDevbox();
|
|
28
|
+
}, []);
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Shutdown Devbox", subtitle: `Shutting down devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Shutting down devbox..." }), success && (_jsx(SuccessMessage, { message: "Devbox shut down successfully!", details: `ID: ${id}` })), error && _jsx(ErrorMessage, { message: "Failed to shutdown devbox", error: error })] }));
|
|
30
|
+
};
|
|
31
|
+
export async function deleteDevbox(id, options = {}) {
|
|
32
|
+
const executor = createExecutor(options);
|
|
33
|
+
await executor.executeDelete(async () => {
|
|
34
|
+
const client = executor.getClient();
|
|
35
|
+
await client.devboxes.shutdown(id);
|
|
36
|
+
}, id, () => _jsx(DeleteDevboxUI, { id: id }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, Box, Text } from 'ink';
|
|
4
|
+
import { getClient } from '../../utils/client.js';
|
|
5
|
+
import { Header } from '../../components/Header.js';
|
|
6
|
+
import { SpinnerComponent } from '../../components/Spinner.js';
|
|
7
|
+
import { ErrorMessage } from '../../components/ErrorMessage.js';
|
|
8
|
+
const ExecCommandUI = ({ id, command, }) => {
|
|
9
|
+
const [loading, setLoading] = React.useState(true);
|
|
10
|
+
const [output, setOutput] = React.useState('');
|
|
11
|
+
const [error, setError] = React.useState(null);
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
const exec = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const client = getClient();
|
|
16
|
+
const result = await client.devboxes.executeSync(id, {
|
|
17
|
+
command: command.join(' '),
|
|
18
|
+
});
|
|
19
|
+
setOutput(result.stdout || result.stderr || 'Command executed successfully');
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
setError(err);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exec();
|
|
29
|
+
}, []);
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Execute Command", subtitle: `Running in devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Executing command..." }), !loading && !error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Box, { borderStyle: "round", borderColor: "green", padding: 1, children: _jsx(Text, { children: output }) }) })), error && _jsx(ErrorMessage, { message: "Failed to execute command", error: error })] }));
|
|
31
|
+
};
|
|
32
|
+
export async function execCommand(id, command) {
|
|
33
|
+
const { waitUntilExit } = render(_jsx(ExecCommandUI, { id: id, command: command }));
|
|
34
|
+
await waitUntilExit();
|
|
35
|
+
}
|