@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,40 @@
|
|
|
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 { createReadStream } from 'fs';
|
|
5
|
+
import { getClient } from '../utils/client.js';
|
|
6
|
+
import { Header } from '../components/Header.js';
|
|
7
|
+
import { SpinnerComponent } from '../components/Spinner.js';
|
|
8
|
+
import { SuccessMessage } from '../components/SuccessMessage.js';
|
|
9
|
+
import { ErrorMessage } from '../components/ErrorMessage.js';
|
|
10
|
+
const UploadFileUI = ({ id, file, targetPath }) => {
|
|
11
|
+
const [loading, setLoading] = React.useState(true);
|
|
12
|
+
const [success, setSuccess] = React.useState(false);
|
|
13
|
+
const [error, setError] = React.useState(null);
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
const upload = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const client = getClient();
|
|
18
|
+
const fileStream = createReadStream(file);
|
|
19
|
+
const filename = file.split('/').pop() || 'uploaded-file';
|
|
20
|
+
await client.devboxes.uploadFile(id, {
|
|
21
|
+
path: targetPath || filename,
|
|
22
|
+
file: fileStream,
|
|
23
|
+
});
|
|
24
|
+
setSuccess(true);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
setError(err);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
upload();
|
|
34
|
+
}, []);
|
|
35
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Upload File", subtitle: `Uploading to devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Uploading file..." }), success && (_jsx(SuccessMessage, { message: "File uploaded successfully!", details: `File: ${file}${targetPath ? `\nTarget: ${targetPath}` : ''}` })), error && _jsx(ErrorMessage, { message: "Failed to upload file", error: error })] }));
|
|
36
|
+
};
|
|
37
|
+
export async function uploadFile(id, file, options) {
|
|
38
|
+
const { waitUntilExit } = render(_jsx(UploadFileUI, { id: id, file: file, targetPath: options.path }));
|
|
39
|
+
await waitUntilExit();
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Gradient from 'ink-gradient';
|
|
5
|
+
export const Banner = React.memo(() => {
|
|
6
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(Gradient, { colors: ['#0a4d3a', '#e5f1ed'], children: _jsx(Text, { bold: true, children: `
|
|
7
|
+
╦═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔═╗
|
|
8
|
+
╠╦╝║ ║║║║║ ║ ║║ ║╠═╝
|
|
9
|
+
╩╚═╚═╝╝╚╝╩═╝╚═╝╚═╝╩ .ai
|
|
10
|
+
` }) }) }));
|
|
11
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import figures from 'figures';
|
|
5
|
+
export const Breadcrumb = React.memo(({ items }) => {
|
|
6
|
+
const baseUrl = process.env.RUNLOOP_BASE_URL;
|
|
7
|
+
const isDevEnvironment = baseUrl && baseUrl !== 'https://api.runloop.ai';
|
|
8
|
+
return (_jsxs(Box, { marginBottom: 1, paddingX: 1, paddingY: 0, children: [_jsx(Text, { color: "green", dimColor: true, bold: true, children: "RL" }), isDevEnvironment && _jsx(Text, { color: "redBright", bold: true, children: " (dev)" }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowRight, " "] }), items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? 'cyan' : 'gray', bold: item.active, dimColor: !item.active, children: item.label }), index < items.length - 1 && (_jsx(Text, { color: "gray", dimColor: true, children: " / " }))] }, index)))] }));
|
|
9
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Reusable detail view component for displaying entity information
|
|
5
|
+
* Organizes data into sections with labeled items
|
|
6
|
+
*/
|
|
7
|
+
export const DetailView = ({ sections }) => {
|
|
8
|
+
return (_jsx(Box, { flexDirection: "column", gap: 1, children: sections.map((section, sectionIndex) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "yellow", bold: true, children: section.title }), section.items.map((item, itemIndex) => (_jsx(Box, { children: _jsxs(Text, { color: item.color || 'gray', dimColor: true, children: [item.label, ": ", item.value] }) }, itemIndex))), sectionIndex < sections.length - 1 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: " " }) }))] }, sectionIndex))) }));
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Helper to build detail sections from an object
|
|
12
|
+
*/
|
|
13
|
+
export function buildDetailSections(data, config) {
|
|
14
|
+
return Object.entries(config).map(([sectionName, sectionConfig]) => ({
|
|
15
|
+
title: sectionName,
|
|
16
|
+
items: sectionConfig.fields
|
|
17
|
+
.map((field) => {
|
|
18
|
+
const value = data[field.key];
|
|
19
|
+
if (value === undefined || value === null)
|
|
20
|
+
return null;
|
|
21
|
+
return {
|
|
22
|
+
label: field.label,
|
|
23
|
+
value: field.formatter ? field.formatter(value) : String(value),
|
|
24
|
+
color: field.color,
|
|
25
|
+
};
|
|
26
|
+
})
|
|
27
|
+
.filter(Boolean),
|
|
28
|
+
})).filter((section) => section.items.length > 0);
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import figures from 'figures';
|
|
4
|
+
export const DevboxCard = ({ id, name, status, createdAt, }) => {
|
|
5
|
+
const getStatusDisplay = (status) => {
|
|
6
|
+
switch (status) {
|
|
7
|
+
case 'running':
|
|
8
|
+
return { icon: figures.tick, color: 'green' };
|
|
9
|
+
case 'provisioning':
|
|
10
|
+
case 'initializing':
|
|
11
|
+
return { icon: figures.ellipsis, color: 'yellow' };
|
|
12
|
+
case 'stopped':
|
|
13
|
+
case 'suspended':
|
|
14
|
+
return { icon: figures.circle, color: 'gray' };
|
|
15
|
+
case 'failed':
|
|
16
|
+
return { icon: figures.cross, color: 'red' };
|
|
17
|
+
default:
|
|
18
|
+
return { icon: figures.circle, color: 'gray' };
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const statusDisplay = getStatusDisplay(status);
|
|
22
|
+
const displayName = name || id;
|
|
23
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: statusDisplay.color, children: statusDisplay.icon }), _jsx(Text, { children: " " }), _jsx(Box, { width: 20, children: _jsx(Text, { color: "cyan", bold: true, children: displayName.slice(0, 18) }) }), _jsx(Text, { color: "gray", dimColor: true, children: id }), createdAt && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "gray", dimColor: true, children: new Date(createdAt).toLocaleDateString() })] }))] }));
|
|
24
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
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 } 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 './Header.js';
|
|
8
|
+
import { SpinnerComponent } from './Spinner.js';
|
|
9
|
+
import { ErrorMessage } from './ErrorMessage.js';
|
|
10
|
+
import { SuccessMessage } from './SuccessMessage.js';
|
|
11
|
+
import { Breadcrumb } from './Breadcrumb.js';
|
|
12
|
+
import { MetadataDisplay } from './MetadataDisplay.js';
|
|
13
|
+
export const DevboxCreatePage = ({ onBack, onCreate }) => {
|
|
14
|
+
const [currentField, setCurrentField] = React.useState('name');
|
|
15
|
+
const [formData, setFormData] = React.useState({
|
|
16
|
+
name: '',
|
|
17
|
+
architecture: 'arm64',
|
|
18
|
+
resource_size: 'SMALL',
|
|
19
|
+
custom_cpu: '',
|
|
20
|
+
custom_memory: '',
|
|
21
|
+
custom_disk: '',
|
|
22
|
+
keep_alive: '3600',
|
|
23
|
+
metadata: {},
|
|
24
|
+
blueprint_id: '',
|
|
25
|
+
snapshot_id: '',
|
|
26
|
+
});
|
|
27
|
+
const [metadataKey, setMetadataKey] = React.useState('');
|
|
28
|
+
const [metadataValue, setMetadataValue] = React.useState('');
|
|
29
|
+
const [inMetadataSection, setInMetadataSection] = React.useState(false);
|
|
30
|
+
const [metadataInputMode, setMetadataInputMode] = React.useState(null);
|
|
31
|
+
const [selectedMetadataIndex, setSelectedMetadataIndex] = React.useState(-1); // -1 means "add new" row
|
|
32
|
+
const [creating, setCreating] = React.useState(false);
|
|
33
|
+
const [result, setResult] = React.useState(null);
|
|
34
|
+
const [error, setError] = React.useState(null);
|
|
35
|
+
const baseFields = [
|
|
36
|
+
{ key: 'name', label: 'Name', type: 'text' },
|
|
37
|
+
{ key: 'architecture', label: 'Architecture', type: 'select' },
|
|
38
|
+
{ key: 'resource_size', label: 'Resource Size', type: 'select' },
|
|
39
|
+
];
|
|
40
|
+
// Add custom resource fields if CUSTOM_SIZE is selected
|
|
41
|
+
const customFields = formData.resource_size === 'CUSTOM_SIZE'
|
|
42
|
+
? [
|
|
43
|
+
{ key: 'custom_cpu', label: 'CPU Cores (2-16, even)', type: 'text' },
|
|
44
|
+
{ key: 'custom_memory', label: 'Memory GB (2-64, even)', type: 'text' },
|
|
45
|
+
{ key: 'custom_disk', label: 'Disk GB (2-64, even)', type: 'text' },
|
|
46
|
+
]
|
|
47
|
+
: [];
|
|
48
|
+
const remainingFields = [
|
|
49
|
+
{ key: 'keep_alive', label: 'Keep Alive (seconds)', type: 'text' },
|
|
50
|
+
{ key: 'blueprint_id', label: 'Blueprint ID (optional)', type: 'text' },
|
|
51
|
+
{ key: 'snapshot_id', label: 'Snapshot ID (optional)', type: 'text' },
|
|
52
|
+
{ key: 'metadata', label: 'Metadata (optional)', type: 'metadata' },
|
|
53
|
+
];
|
|
54
|
+
const fields = [...baseFields, ...customFields, ...remainingFields];
|
|
55
|
+
const architectures = ['arm64', 'x86_64'];
|
|
56
|
+
const resourceSizes = ['X_SMALL', 'SMALL', 'MEDIUM', 'LARGE', 'X_LARGE', 'XX_LARGE', 'CUSTOM_SIZE'];
|
|
57
|
+
const currentFieldIndex = fields.findIndex((f) => f.key === currentField);
|
|
58
|
+
useInput((input, key) => {
|
|
59
|
+
// Handle result screen
|
|
60
|
+
if (result) {
|
|
61
|
+
if (input === 'q' || key.escape || key.return) {
|
|
62
|
+
if (onCreate) {
|
|
63
|
+
onCreate(result);
|
|
64
|
+
}
|
|
65
|
+
onBack();
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Handle error screen
|
|
70
|
+
if (error) {
|
|
71
|
+
if (input === 'r' || key.return) {
|
|
72
|
+
// Retry - clear error and return to form
|
|
73
|
+
setError(null);
|
|
74
|
+
}
|
|
75
|
+
else if (input === 'q' || key.escape) {
|
|
76
|
+
// Quit - go back to list
|
|
77
|
+
onBack();
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Handle creating state
|
|
82
|
+
if (creating) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Back to list
|
|
86
|
+
if (input === 'q' || key.escape) {
|
|
87
|
+
console.clear();
|
|
88
|
+
onBack();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Submit form
|
|
92
|
+
if (input === 's' && key.ctrl) {
|
|
93
|
+
handleCreate();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Handle metadata section
|
|
97
|
+
if (inMetadataSection) {
|
|
98
|
+
const metadataKeys = Object.keys(formData.metadata);
|
|
99
|
+
// Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
|
|
100
|
+
const maxIndex = metadataKeys.length + 1;
|
|
101
|
+
// Handle input mode (typing key or value)
|
|
102
|
+
if (metadataInputMode) {
|
|
103
|
+
if (metadataInputMode === 'key' && key.return && metadataKey.trim()) {
|
|
104
|
+
setMetadataInputMode('value');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
else if (metadataInputMode === 'value' && key.return) {
|
|
108
|
+
if (metadataKey.trim() && metadataValue.trim()) {
|
|
109
|
+
setFormData({
|
|
110
|
+
...formData,
|
|
111
|
+
metadata: {
|
|
112
|
+
...formData.metadata,
|
|
113
|
+
[metadataKey.trim()]: metadataValue.trim(),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
setMetadataKey('');
|
|
118
|
+
setMetadataValue('');
|
|
119
|
+
setMetadataInputMode(null);
|
|
120
|
+
setSelectedMetadataIndex(0); // Back to "add new" row
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
else if (key.escape) {
|
|
124
|
+
// Cancel input
|
|
125
|
+
setMetadataKey('');
|
|
126
|
+
setMetadataValue('');
|
|
127
|
+
setMetadataInputMode(null);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
else if (key.tab) {
|
|
131
|
+
// Tab between key and value
|
|
132
|
+
setMetadataInputMode(metadataInputMode === 'key' ? 'value' : 'key');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
return; // Don't process other keys while in input mode
|
|
136
|
+
}
|
|
137
|
+
// Navigation mode
|
|
138
|
+
if (key.upArrow && selectedMetadataIndex > 0) {
|
|
139
|
+
setSelectedMetadataIndex(selectedMetadataIndex - 1);
|
|
140
|
+
}
|
|
141
|
+
else if (key.downArrow && selectedMetadataIndex < maxIndex) {
|
|
142
|
+
setSelectedMetadataIndex(selectedMetadataIndex + 1);
|
|
143
|
+
}
|
|
144
|
+
else if (key.return) {
|
|
145
|
+
if (selectedMetadataIndex === 0) {
|
|
146
|
+
// Add new
|
|
147
|
+
setMetadataKey('');
|
|
148
|
+
setMetadataValue('');
|
|
149
|
+
setMetadataInputMode('key');
|
|
150
|
+
}
|
|
151
|
+
else if (selectedMetadataIndex === maxIndex) {
|
|
152
|
+
// Done - exit metadata section
|
|
153
|
+
setInMetadataSection(false);
|
|
154
|
+
setSelectedMetadataIndex(0);
|
|
155
|
+
setMetadataKey('');
|
|
156
|
+
setMetadataValue('');
|
|
157
|
+
setMetadataInputMode(null);
|
|
158
|
+
}
|
|
159
|
+
else if (selectedMetadataIndex >= 1 && selectedMetadataIndex <= metadataKeys.length) {
|
|
160
|
+
// Edit existing (selectedMetadataIndex - 1 gives array index)
|
|
161
|
+
const keyToEdit = metadataKeys[selectedMetadataIndex - 1];
|
|
162
|
+
setMetadataKey(keyToEdit || '');
|
|
163
|
+
setMetadataValue(formData.metadata[keyToEdit] || '');
|
|
164
|
+
// Remove old entry
|
|
165
|
+
const newMetadata = { ...formData.metadata };
|
|
166
|
+
delete newMetadata[keyToEdit];
|
|
167
|
+
setFormData({ ...formData, metadata: newMetadata });
|
|
168
|
+
setMetadataInputMode('key');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if ((input === 'd' || key.delete) && selectedMetadataIndex >= 1 && selectedMetadataIndex <= metadataKeys.length) {
|
|
172
|
+
// Delete selected item (selectedMetadataIndex - 1 gives array index)
|
|
173
|
+
const keyToDelete = metadataKeys[selectedMetadataIndex - 1];
|
|
174
|
+
const newMetadata = { ...formData.metadata };
|
|
175
|
+
delete newMetadata[keyToDelete];
|
|
176
|
+
setFormData({ ...formData, metadata: newMetadata });
|
|
177
|
+
// Stay at same position or move to add new if we deleted the last item
|
|
178
|
+
const newLength = Object.keys(newMetadata).length;
|
|
179
|
+
if (selectedMetadataIndex > newLength) {
|
|
180
|
+
setSelectedMetadataIndex(Math.max(0, newLength));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (key.escape || input === 'q') {
|
|
184
|
+
// Exit metadata section
|
|
185
|
+
setInMetadataSection(false);
|
|
186
|
+
setSelectedMetadataIndex(0);
|
|
187
|
+
setMetadataKey('');
|
|
188
|
+
setMetadataValue('');
|
|
189
|
+
setMetadataInputMode(null);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Now safe to get field from list
|
|
194
|
+
const field = fields[currentFieldIndex];
|
|
195
|
+
// Navigation
|
|
196
|
+
if (key.upArrow && currentFieldIndex > 0) {
|
|
197
|
+
setCurrentField(fields[currentFieldIndex - 1].key);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (key.downArrow && currentFieldIndex < fields.length - 1) {
|
|
201
|
+
setCurrentField(fields[currentFieldIndex + 1].key);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Enter key on metadata field to enter metadata section
|
|
205
|
+
if (currentField === 'metadata' && key.return) {
|
|
206
|
+
setInMetadataSection(true);
|
|
207
|
+
setSelectedMetadataIndex(0); // Start at "add new" row
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Handle select fields
|
|
211
|
+
if (field && field.type === 'select' && (key.leftArrow || key.rightArrow)) {
|
|
212
|
+
if (currentField === 'architecture') {
|
|
213
|
+
const currentIndex = architectures.indexOf(formData.architecture);
|
|
214
|
+
const newIndex = key.leftArrow
|
|
215
|
+
? Math.max(0, currentIndex - 1)
|
|
216
|
+
: Math.min(architectures.length - 1, currentIndex + 1);
|
|
217
|
+
setFormData({ ...formData, architecture: architectures[newIndex] });
|
|
218
|
+
}
|
|
219
|
+
else if (currentField === 'resource_size') {
|
|
220
|
+
const currentIndex = resourceSizes.indexOf(formData.resource_size);
|
|
221
|
+
const newIndex = key.leftArrow
|
|
222
|
+
? Math.max(0, currentIndex - 1)
|
|
223
|
+
: Math.min(resourceSizes.length - 1, currentIndex + 1);
|
|
224
|
+
setFormData({ ...formData, resource_size: resourceSizes[newIndex] });
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// Validate custom resource configuration
|
|
230
|
+
const validateCustomResources = () => {
|
|
231
|
+
if (formData.resource_size !== 'CUSTOM_SIZE') {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const cpu = parseInt(formData.custom_cpu);
|
|
235
|
+
const memory = parseInt(formData.custom_memory);
|
|
236
|
+
const disk = parseInt(formData.custom_disk);
|
|
237
|
+
if (formData.custom_cpu && (isNaN(cpu) || cpu < 2 || cpu > 16 || cpu % 2 !== 0)) {
|
|
238
|
+
return 'CPU cores must be an even number between 2 and 16';
|
|
239
|
+
}
|
|
240
|
+
if (formData.custom_memory && (isNaN(memory) || memory < 2 || memory > 64 || memory % 2 !== 0)) {
|
|
241
|
+
return 'Memory must be an even number between 2 and 64 GB';
|
|
242
|
+
}
|
|
243
|
+
if (formData.custom_disk && (isNaN(disk) || disk < 2 || disk > 64 || disk % 2 !== 0)) {
|
|
244
|
+
return 'Disk must be an even number between 2 and 64 GB';
|
|
245
|
+
}
|
|
246
|
+
// Validate CPU to memory ratio (1:2 to 1:8)
|
|
247
|
+
if (formData.custom_cpu && formData.custom_memory) {
|
|
248
|
+
const ratio = memory / cpu;
|
|
249
|
+
if (ratio < 2 || ratio > 8) {
|
|
250
|
+
return `CPU to memory ratio must be 1:2 to 1:8 (got ${cpu}:${memory}, ratio 1:${ratio.toFixed(1)})`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
const handleCreate = async () => {
|
|
256
|
+
// Validate before creating
|
|
257
|
+
const validationError = validateCustomResources();
|
|
258
|
+
if (validationError) {
|
|
259
|
+
setError(new Error(validationError));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
setCreating(true);
|
|
263
|
+
setError(null);
|
|
264
|
+
try {
|
|
265
|
+
const client = getClient();
|
|
266
|
+
const launchParameters = {};
|
|
267
|
+
if (formData.architecture) {
|
|
268
|
+
launchParameters.architecture = formData.architecture;
|
|
269
|
+
}
|
|
270
|
+
if (formData.resource_size) {
|
|
271
|
+
launchParameters.resource_size_request = formData.resource_size;
|
|
272
|
+
}
|
|
273
|
+
if (formData.resource_size === 'CUSTOM_SIZE') {
|
|
274
|
+
if (formData.custom_cpu)
|
|
275
|
+
launchParameters.custom_cpu_cores = parseInt(formData.custom_cpu);
|
|
276
|
+
if (formData.custom_memory)
|
|
277
|
+
launchParameters.custom_gb_memory = parseInt(formData.custom_memory);
|
|
278
|
+
if (formData.custom_disk)
|
|
279
|
+
launchParameters.custom_disk_size = parseInt(formData.custom_disk);
|
|
280
|
+
}
|
|
281
|
+
if (formData.keep_alive) {
|
|
282
|
+
launchParameters.keep_alive_time_seconds = parseInt(formData.keep_alive);
|
|
283
|
+
}
|
|
284
|
+
const createParams = {};
|
|
285
|
+
if (formData.name) {
|
|
286
|
+
createParams.name = formData.name;
|
|
287
|
+
}
|
|
288
|
+
if (Object.keys(formData.metadata).length > 0) {
|
|
289
|
+
createParams.metadata = formData.metadata;
|
|
290
|
+
}
|
|
291
|
+
if (formData.blueprint_id) {
|
|
292
|
+
createParams.blueprint_id = formData.blueprint_id;
|
|
293
|
+
}
|
|
294
|
+
if (formData.snapshot_id) {
|
|
295
|
+
createParams.snapshot_id = formData.snapshot_id;
|
|
296
|
+
}
|
|
297
|
+
if (Object.keys(launchParameters).length > 0) {
|
|
298
|
+
createParams.launch_parameters = launchParameters;
|
|
299
|
+
}
|
|
300
|
+
const devbox = await client.devboxes.create(createParams);
|
|
301
|
+
setResult(devbox);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
setError(err);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
setCreating(false);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
// Result screen
|
|
311
|
+
if (result) {
|
|
312
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
313
|
+
{ label: 'Devboxes' },
|
|
314
|
+
{ label: 'Create', active: true }
|
|
315
|
+
] }), _jsx(Header, { title: "Create Devbox" }), _jsx(SuccessMessage, { message: "Devbox created successfully!", details: `ID: ${result.id}\nName: ${result.name || '(none)'}\nStatus: ${result.status}` }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to return to list" }) })] }));
|
|
316
|
+
}
|
|
317
|
+
// Error screen
|
|
318
|
+
if (error) {
|
|
319
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
320
|
+
{ label: 'Devboxes' },
|
|
321
|
+
{ label: 'Create', active: true }
|
|
322
|
+
] }), _jsx(Header, { title: "Create Devbox" }), _jsx(ErrorMessage, { message: "Failed to create devbox", error: error }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] or [r] to retry \u2022 [q] or [esc] to cancel" }) })] }));
|
|
323
|
+
}
|
|
324
|
+
// Creating screen
|
|
325
|
+
if (creating) {
|
|
326
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
327
|
+
{ label: 'Devboxes' },
|
|
328
|
+
{ label: 'Create', active: true }
|
|
329
|
+
] }), _jsx(Header, { title: "Create Devbox" }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
|
|
330
|
+
}
|
|
331
|
+
// Form screen
|
|
332
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
333
|
+
{ label: 'Devboxes' },
|
|
334
|
+
{ label: 'Create', active: true }
|
|
335
|
+
] }), _jsx(Header, { title: "Create Devbox" }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field, index) => {
|
|
336
|
+
const isActive = currentField === field.key;
|
|
337
|
+
const fieldData = formData[field.key];
|
|
338
|
+
if (field.type === 'text') {
|
|
339
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? 'cyan' : 'gray', children: [isActive ? figures.pointer : ' ', " ", field.label, ":", ' '] }), isActive ? (_jsx(TextInput, { value: String(fieldData || ''), onChange: (value) => {
|
|
340
|
+
setFormData({ ...formData, [field.key]: value });
|
|
341
|
+
}, placeholder: field.key === 'name' ? 'my-devbox' :
|
|
342
|
+
field.key === 'keep_alive' ? '3600' :
|
|
343
|
+
field.key === 'blueprint_id' ? 'bp_xxx' :
|
|
344
|
+
field.key === 'snapshot_id' ? 'snap_xxx' :
|
|
345
|
+
'' })) : (_jsx(Text, { color: "white", children: String(fieldData || '(empty)') }))] }, field.key));
|
|
346
|
+
}
|
|
347
|
+
if (field.type === 'select') {
|
|
348
|
+
const value = fieldData;
|
|
349
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? 'cyan' : 'gray', children: [isActive ? figures.pointer : ' ', " ", field.label, ":"] }), _jsxs(Text, { color: isActive ? 'cyan' : 'white', bold: isActive, children: [' ', value || '(none)'] }), isActive && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[", figures.arrowLeft, figures.arrowRight, " to change]"] }))] }, field.key));
|
|
350
|
+
}
|
|
351
|
+
if (field.type === 'metadata') {
|
|
352
|
+
if (!inMetadataSection) {
|
|
353
|
+
// Collapsed view
|
|
354
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? 'cyan' : 'gray', children: [isActive ? figures.pointer : ' ', " ", field.label, ":", ' '] }), _jsxs(Text, { color: "white", children: [Object.keys(formData.metadata).length, " item(s)"] }), isActive && (_jsx(Text, { color: "gray", dimColor: true, children: " [Enter to manage]" }))] }), Object.keys(formData.metadata).length > 0 && (_jsx(Box, { marginLeft: 2, children: _jsx(MetadataDisplay, { metadata: formData.metadata, showBorder: false }) }))] }, field.key));
|
|
355
|
+
}
|
|
356
|
+
// Expanded metadata section view
|
|
357
|
+
const metadataKeys = Object.keys(formData.metadata);
|
|
358
|
+
// Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
|
|
359
|
+
const maxIndex = metadataKeys.length + 1;
|
|
360
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " Manage Metadata"] }), metadataInputMode && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: selectedMetadataIndex === 0 ? 'green' : 'yellow', paddingX: 1, children: [_jsx(Text, { color: selectedMetadataIndex === 0 ? 'green' : 'yellow', bold: true, children: selectedMetadataIndex === 0 ? 'Adding New' : 'Editing' }), _jsx(Box, { children: metadataInputMode === 'key' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: "Key: " }), _jsx(TextInput, { value: metadataKey || '', onChange: setMetadataKey, placeholder: "env" })] })) : (_jsxs(Text, { dimColor: true, children: ["Key: ", metadataKey || ''] })) }), _jsx(Box, { children: metadataInputMode === 'value' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", children: "Value: " }), _jsx(TextInput, { value: metadataValue || '', onChange: setMetadataValue, placeholder: "production" })] })) : (_jsxs(Text, { dimColor: true, children: ["Value: ", metadataValue || ''] })) })] })), !metadataInputMode && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedMetadataIndex === 0 ? 'cyan' : 'gray', children: [selectedMetadataIndex === 0 ? figures.pointer : ' ', ' '] }), _jsx(Text, { color: selectedMetadataIndex === 0 ? 'green' : 'gray', bold: selectedMetadataIndex === 0, children: "+ Add new metadata" })] }), metadataKeys.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: metadataKeys.map((key, index) => {
|
|
361
|
+
const itemIndex = index + 1; // Items are at indices 1..n
|
|
362
|
+
const isSelected = selectedMetadataIndex === itemIndex;
|
|
363
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', ' '] }), _jsxs(Text, { color: isSelected ? 'cyan' : 'gray', bold: isSelected, children: [key, ": ", formData.metadata[key]] })] }, key));
|
|
364
|
+
}) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedMetadataIndex === maxIndex ? 'cyan' : 'gray', children: [selectedMetadataIndex === maxIndex ? figures.pointer : ' ', ' '] }), _jsxs(Text, { color: selectedMetadataIndex === maxIndex ? 'green' : 'gray', bold: selectedMetadataIndex === maxIndex, children: [figures.tick, " Done"] })] })] })), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: metadataInputMode
|
|
365
|
+
? `[Tab] Switch field • [Enter] ${metadataInputMode === 'key' ? 'Next' : 'Save'} • [esc] Cancel`
|
|
366
|
+
: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedMetadataIndex === 0 ? 'Add' : selectedMetadataIndex === maxIndex ? 'Done' : 'Edit'} • [d] Delete • [esc] Back` }) })] }, field.key));
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}) }), formData.resource_size === 'CUSTOM_SIZE' && validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: "red", dimColor: true, children: validateCustomResources() })] })), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: "green", bold: true, children: [figures.info, " Summary"] }), _jsxs(Text, { dimColor: true, children: ["Name: ", formData.name || '(auto-generated)'] }), _jsxs(Text, { dimColor: true, children: ["Architecture: ", formData.architecture] }), formData.resource_size && _jsxs(Text, { dimColor: true, children: ["Resources: ", formData.resource_size] }), formData.resource_size === 'CUSTOM_SIZE' && formData.custom_cpu && (_jsxs(Text, { dimColor: true, children: [" CPU: ", formData.custom_cpu, " cores"] })), formData.resource_size === 'CUSTOM_SIZE' && formData.custom_memory && (_jsxs(Text, { dimColor: true, children: [" Memory: ", formData.custom_memory, " GB"] })), formData.resource_size === 'CUSTOM_SIZE' && formData.custom_disk && (_jsxs(Text, { dimColor: true, children: [" Disk: ", formData.custom_disk, " GB"] })), _jsxs(Text, { dimColor: true, children: ["Keep Alive: ", formData.keep_alive, "s (", Math.floor(parseInt(formData.keep_alive || '0') / 60), "m)"] }), formData.blueprint_id && _jsxs(Text, { dimColor: true, children: ["Blueprint: ", formData.blueprint_id] }), formData.snapshot_id && _jsxs(Text, { dimColor: true, children: ["Snapshot: ", formData.snapshot_id] }), _jsxs(Text, { dimColor: true, children: ["Metadata: ", Object.keys(formData.metadata).length, " item(s)"] })] }), !inMetadataSection && (_jsxs(_Fragment, { children: [_jsx(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, paddingY: 0, marginTop: 1, children: _jsxs(Text, { color: "green", bold: true, children: [figures.play, " Press [Ctrl+S] to create this devbox"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Ctrl+S] Create \u2022 [q] Cancel"] }) })] }))] }));
|
|
370
|
+
};
|