@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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/dist/cli.js +110 -0
  4. package/dist/commands/auth.js +27 -0
  5. package/dist/commands/blueprint/list.js +373 -0
  6. package/dist/commands/create.js +42 -0
  7. package/dist/commands/delete.js +34 -0
  8. package/dist/commands/devbox/create.js +45 -0
  9. package/dist/commands/devbox/delete.js +37 -0
  10. package/dist/commands/devbox/exec.js +35 -0
  11. package/dist/commands/devbox/list.js +302 -0
  12. package/dist/commands/devbox/upload.js +40 -0
  13. package/dist/commands/exec.js +35 -0
  14. package/dist/commands/list.js +59 -0
  15. package/dist/commands/snapshot/create.js +40 -0
  16. package/dist/commands/snapshot/delete.js +37 -0
  17. package/dist/commands/snapshot/list.js +122 -0
  18. package/dist/commands/upload.js +40 -0
  19. package/dist/components/Banner.js +11 -0
  20. package/dist/components/Breadcrumb.js +9 -0
  21. package/dist/components/DetailView.js +29 -0
  22. package/dist/components/DevboxCard.js +24 -0
  23. package/dist/components/DevboxCreatePage.js +370 -0
  24. package/dist/components/DevboxDetailPage.js +621 -0
  25. package/dist/components/ErrorMessage.js +6 -0
  26. package/dist/components/Header.js +6 -0
  27. package/dist/components/MetadataDisplay.js +24 -0
  28. package/dist/components/OperationsMenu.js +19 -0
  29. package/dist/components/Spinner.js +6 -0
  30. package/dist/components/StatusBadge.js +41 -0
  31. package/dist/components/SuccessMessage.js +6 -0
  32. package/dist/components/Table.example.js +55 -0
  33. package/dist/components/Table.js +54 -0
  34. package/dist/utils/CommandExecutor.js +115 -0
  35. package/dist/utils/client.js +13 -0
  36. package/dist/utils/config.js +17 -0
  37. package/dist/utils/output.js +115 -0
  38. 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
+ };