@runloop/rl-cli 1.3.0 ā 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/blueprint/from-dockerfile.js +182 -0
- package/dist/components/DevboxCreatePage.js +103 -111
- package/dist/components/NetworkPolicyCreatePage.js +11 -12
- package/dist/components/form/FormTextInput.js +2 -2
- package/dist/screens/BlueprintDetailScreen.js +20 -2
- package/dist/screens/DevboxDetailScreen.js +4 -4
- package/dist/screens/NetworkPolicyDetailScreen.js +2 -2
- package/dist/screens/ObjectDetailScreen.js +2 -2
- package/dist/screens/SnapshotDetailScreen.js +2 -2
- package/dist/utils/commands.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ rli blueprint create # Create a new blueprint
|
|
|
118
118
|
rli blueprint get <name-or-id> # Get blueprint details by name or ID (...
|
|
119
119
|
rli blueprint logs <name-or-id> # Get blueprint build logs by name or I...
|
|
120
120
|
rli blueprint prune <name> # Delete old blueprint builds, keeping ...
|
|
121
|
+
rli blueprint from-dockerfile # Create a blueprint from a Dockerfile ...
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
### Object Commands (alias: `obj`)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create blueprint from Dockerfile command
|
|
3
|
+
*
|
|
4
|
+
* Manually handles each step with progress feedback:
|
|
5
|
+
* 1. Upload build context as tarball
|
|
6
|
+
* 2. Create blueprint
|
|
7
|
+
* 3. Poll for build completion
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, stat } from "fs/promises";
|
|
10
|
+
import { resolve, join } from "path";
|
|
11
|
+
import { getClient } from "../../utils/client.js";
|
|
12
|
+
import { output, outputError } from "../../utils/output.js";
|
|
13
|
+
import { StorageObject } from "@runloop/api-client/sdk";
|
|
14
|
+
// Helper to check if we should show progress
|
|
15
|
+
function shouldShowProgress(options) {
|
|
16
|
+
return !options.output || options.output === "text";
|
|
17
|
+
}
|
|
18
|
+
// Helper to log progress (to stderr so it doesn't interfere with JSON output)
|
|
19
|
+
function logProgress(message, options) {
|
|
20
|
+
if (shouldShowProgress(options)) {
|
|
21
|
+
console.error(message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Helper to format elapsed time
|
|
25
|
+
function formatElapsed(startTime) {
|
|
26
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
27
|
+
if (elapsed < 60) {
|
|
28
|
+
return `${elapsed}s`;
|
|
29
|
+
}
|
|
30
|
+
const minutes = Math.floor(elapsed / 60);
|
|
31
|
+
const seconds = elapsed % 60;
|
|
32
|
+
return `${minutes}m ${seconds}s`;
|
|
33
|
+
}
|
|
34
|
+
export async function createBlueprintFromDockerfile(options) {
|
|
35
|
+
const startTime = Date.now();
|
|
36
|
+
try {
|
|
37
|
+
const client = getClient();
|
|
38
|
+
// Resolve build context path (defaults to current directory)
|
|
39
|
+
const buildContextPath = resolve(options.buildContext || ".");
|
|
40
|
+
// Verify build context exists and is a directory
|
|
41
|
+
try {
|
|
42
|
+
const stats = await stat(buildContextPath);
|
|
43
|
+
if (!stats.isDirectory()) {
|
|
44
|
+
outputError(`Build context path is not a directory: ${buildContextPath}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
outputError(`Build context path does not exist: ${buildContextPath}`);
|
|
49
|
+
}
|
|
50
|
+
// Resolve Dockerfile path
|
|
51
|
+
const dockerfilePath = options.dockerfile
|
|
52
|
+
? resolve(options.dockerfile)
|
|
53
|
+
: join(buildContextPath, "Dockerfile");
|
|
54
|
+
// Verify Dockerfile exists
|
|
55
|
+
try {
|
|
56
|
+
const stats = await stat(dockerfilePath);
|
|
57
|
+
if (!stats.isFile()) {
|
|
58
|
+
outputError(`Dockerfile path is not a file: ${dockerfilePath}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
outputError(`Dockerfile not found: ${dockerfilePath}`);
|
|
63
|
+
}
|
|
64
|
+
// Log initial info
|
|
65
|
+
logProgress(`\nš¦ Creating blueprint "${options.name}" from Dockerfile`, options);
|
|
66
|
+
logProgress(` Build context: ${buildContextPath}`, options);
|
|
67
|
+
logProgress(` Dockerfile: ${dockerfilePath}\n`, options);
|
|
68
|
+
// Read the Dockerfile contents
|
|
69
|
+
const dockerfileContents = await readFile(dockerfilePath, "utf-8");
|
|
70
|
+
// Parse user parameters
|
|
71
|
+
let userParameters = undefined;
|
|
72
|
+
if (options.user && options.root) {
|
|
73
|
+
outputError("Only one of --user or --root can be specified");
|
|
74
|
+
}
|
|
75
|
+
else if (options.user) {
|
|
76
|
+
const [username, uid] = options.user.split(":");
|
|
77
|
+
if (!username || !uid) {
|
|
78
|
+
outputError("User must be in format 'username:uid'");
|
|
79
|
+
}
|
|
80
|
+
userParameters = { username, uid: parseInt(uid) };
|
|
81
|
+
}
|
|
82
|
+
else if (options.root) {
|
|
83
|
+
userParameters = { username: "root", uid: 0 };
|
|
84
|
+
}
|
|
85
|
+
// Build launch parameters
|
|
86
|
+
const launchParameters = {};
|
|
87
|
+
if (options.resources) {
|
|
88
|
+
launchParameters.resource_size_request = options.resources;
|
|
89
|
+
}
|
|
90
|
+
if (options.architecture) {
|
|
91
|
+
launchParameters.architecture = options.architecture;
|
|
92
|
+
}
|
|
93
|
+
if (options.availablePorts) {
|
|
94
|
+
launchParameters.available_ports = options.availablePorts.map((port) => parseInt(port, 10));
|
|
95
|
+
}
|
|
96
|
+
if (userParameters) {
|
|
97
|
+
launchParameters.user_parameters = userParameters;
|
|
98
|
+
}
|
|
99
|
+
// Parse TTL (default: 1 hour = 3600000ms)
|
|
100
|
+
const ttlMs = options.ttl ? parseInt(options.ttl) * 1000 : 3600000;
|
|
101
|
+
// Step 1: Upload build context
|
|
102
|
+
logProgress(`ā³ [1/3] Creating and uploading build context tarball...`, options);
|
|
103
|
+
const uploadStart = Date.now();
|
|
104
|
+
const storageObject = await StorageObject.uploadFromDir(client, buildContextPath, {
|
|
105
|
+
name: `build-context-${options.name}`,
|
|
106
|
+
ttl_ms: ttlMs,
|
|
107
|
+
});
|
|
108
|
+
logProgress(`ā
[1/3] Build context uploaded (${formatElapsed(uploadStart)})`, options);
|
|
109
|
+
logProgress(` Object ID: ${storageObject.id}`, options);
|
|
110
|
+
// Step 2: Create the blueprint
|
|
111
|
+
logProgress(`\nā³ [2/3] Creating blueprint...`, options);
|
|
112
|
+
const createStart = Date.now();
|
|
113
|
+
const createParams = {
|
|
114
|
+
name: options.name,
|
|
115
|
+
dockerfile: dockerfileContents,
|
|
116
|
+
system_setup_commands: options.systemSetupCommands,
|
|
117
|
+
launch_parameters: launchParameters,
|
|
118
|
+
build_context: {
|
|
119
|
+
type: "object",
|
|
120
|
+
object_id: storageObject.id,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const blueprintResponse = await client.blueprints.create(createParams);
|
|
124
|
+
logProgress(`ā
[2/3] Blueprint created (${formatElapsed(createStart)})`, options);
|
|
125
|
+
logProgress(` Blueprint ID: ${blueprintResponse.id}`, options);
|
|
126
|
+
// Step 3: Wait for build to complete (unless --no-wait)
|
|
127
|
+
if (options.noWait) {
|
|
128
|
+
logProgress(`\nā© Skipping build wait (--no-wait specified)`, options);
|
|
129
|
+
logProgress(` Check status with: rli blueprint get ${blueprintResponse.id}`, options);
|
|
130
|
+
logProgress(` View logs with: rli blueprint logs ${blueprintResponse.id}\n`, options);
|
|
131
|
+
output(blueprintResponse, {
|
|
132
|
+
format: options.output,
|
|
133
|
+
defaultFormat: "json",
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
logProgress(`\nā³ [3/3] Waiting for build to complete...`, options);
|
|
138
|
+
const buildStart = Date.now();
|
|
139
|
+
let lastStatus = "";
|
|
140
|
+
let pollCount = 0;
|
|
141
|
+
// Poll for completion
|
|
142
|
+
while (true) {
|
|
143
|
+
const blueprint = await client.blueprints.retrieve(blueprintResponse.id);
|
|
144
|
+
const currentStatus = blueprint.status || "unknown";
|
|
145
|
+
// Log status changes
|
|
146
|
+
if (currentStatus !== lastStatus) {
|
|
147
|
+
const elapsed = formatElapsed(buildStart);
|
|
148
|
+
logProgress(` Status: ${currentStatus} (${elapsed})`, options);
|
|
149
|
+
lastStatus = currentStatus;
|
|
150
|
+
}
|
|
151
|
+
else if (pollCount % 10 === 0 && pollCount > 0) {
|
|
152
|
+
// Log periodic updates even without status change (every ~30s)
|
|
153
|
+
const elapsed = formatElapsed(buildStart);
|
|
154
|
+
logProgress(` Still ${currentStatus}... (${elapsed})`, options);
|
|
155
|
+
}
|
|
156
|
+
// Check for terminal states
|
|
157
|
+
if (blueprint.status === "build_complete") {
|
|
158
|
+
logProgress(`\nā
[3/3] Build completed successfully! (${formatElapsed(buildStart)})`, options);
|
|
159
|
+
logProgress(`\nš Blueprint "${options.name}" is ready!`, options);
|
|
160
|
+
logProgress(` Total time: ${formatElapsed(startTime)}`, options);
|
|
161
|
+
logProgress(` Blueprint ID: ${blueprint.id}\n`, options);
|
|
162
|
+
output(blueprint, { format: options.output, defaultFormat: "json" });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (blueprint.status === "failed") {
|
|
166
|
+
logProgress(`\nā [3/3] Build failed (${formatElapsed(buildStart)})`, options);
|
|
167
|
+
if (blueprint.failure_reason) {
|
|
168
|
+
logProgress(` Reason: ${blueprint.failure_reason}`, options);
|
|
169
|
+
}
|
|
170
|
+
logProgress(` View logs with: rli blueprint logs ${blueprint.id}\n`, options);
|
|
171
|
+
outputError(`Blueprint build failed. Status: ${blueprint.status}`);
|
|
172
|
+
}
|
|
173
|
+
// Wait before next poll (3 seconds)
|
|
174
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
175
|
+
pollCount++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
logProgress(`\nā Failed after ${formatElapsed(startTime)}`, options);
|
|
180
|
+
outputError("Failed to create blueprint from Dockerfile", error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -109,6 +109,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
109
109
|
// Select navigation handlers using shared hook
|
|
110
110
|
const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
|
|
111
111
|
const handleResourceSizeNav = useFormSelectNavigation(formData.resource_size || "SMALL", resourceSizes, (value) => setFormData({ ...formData, resource_size: value }), currentField === "resource_size");
|
|
112
|
+
// Main form input handler - active when not in metadata section
|
|
112
113
|
useInput((input, key) => {
|
|
113
114
|
// Handle result screen
|
|
114
115
|
if (result) {
|
|
@@ -116,7 +117,9 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
116
117
|
if (onCreate) {
|
|
117
118
|
onCreate(result);
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
else {
|
|
121
|
+
onBack();
|
|
122
|
+
}
|
|
120
123
|
}
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
@@ -136,118 +139,24 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
136
139
|
if (creating) {
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
139
|
-
//
|
|
140
|
-
if (inMetadataSection) {
|
|
141
|
-
const metadataKeys = Object.keys(formData.metadata);
|
|
142
|
-
// Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
|
|
143
|
-
const maxIndex = metadataKeys.length + 1;
|
|
144
|
-
// Handle input mode (typing key or value)
|
|
145
|
-
if (metadataInputMode) {
|
|
146
|
-
if (metadataInputMode === "key" && key.return && metadataKey.trim()) {
|
|
147
|
-
setMetadataInputMode("value");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
else if (metadataInputMode === "value" && key.return) {
|
|
151
|
-
if (metadataKey.trim() && metadataValue.trim()) {
|
|
152
|
-
setFormData({
|
|
153
|
-
...formData,
|
|
154
|
-
metadata: {
|
|
155
|
-
...formData.metadata,
|
|
156
|
-
[metadataKey.trim()]: metadataValue.trim(),
|
|
157
|
-
},
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
setMetadataKey("");
|
|
161
|
-
setMetadataValue("");
|
|
162
|
-
setMetadataInputMode(null);
|
|
163
|
-
setSelectedMetadataIndex(0); // Back to "add new" row
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
else if (key.escape) {
|
|
167
|
-
// Cancel input
|
|
168
|
-
setMetadataKey("");
|
|
169
|
-
setMetadataValue("");
|
|
170
|
-
setMetadataInputMode(null);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
else if (key.tab) {
|
|
174
|
-
// Tab between key and value
|
|
175
|
-
setMetadataInputMode(metadataInputMode === "key" ? "value" : "key");
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
return; // Don't process other keys while in input mode
|
|
179
|
-
}
|
|
180
|
-
// Navigation mode
|
|
181
|
-
if (key.upArrow && selectedMetadataIndex > 0) {
|
|
182
|
-
setSelectedMetadataIndex(selectedMetadataIndex - 1);
|
|
183
|
-
}
|
|
184
|
-
else if (key.downArrow && selectedMetadataIndex < maxIndex) {
|
|
185
|
-
setSelectedMetadataIndex(selectedMetadataIndex + 1);
|
|
186
|
-
}
|
|
187
|
-
else if (key.return) {
|
|
188
|
-
if (selectedMetadataIndex === 0) {
|
|
189
|
-
// Add new
|
|
190
|
-
setMetadataKey("");
|
|
191
|
-
setMetadataValue("");
|
|
192
|
-
setMetadataInputMode("key");
|
|
193
|
-
}
|
|
194
|
-
else if (selectedMetadataIndex === maxIndex) {
|
|
195
|
-
// Done - exit metadata section
|
|
196
|
-
setInMetadataSection(false);
|
|
197
|
-
setSelectedMetadataIndex(0);
|
|
198
|
-
setMetadataKey("");
|
|
199
|
-
setMetadataValue("");
|
|
200
|
-
setMetadataInputMode(null);
|
|
201
|
-
}
|
|
202
|
-
else if (selectedMetadataIndex >= 1 &&
|
|
203
|
-
selectedMetadataIndex <= metadataKeys.length) {
|
|
204
|
-
// Edit existing (selectedMetadataIndex - 1 gives array index)
|
|
205
|
-
const keyToEdit = metadataKeys[selectedMetadataIndex - 1];
|
|
206
|
-
setMetadataKey(keyToEdit || "");
|
|
207
|
-
setMetadataValue(formData.metadata[keyToEdit] || "");
|
|
208
|
-
// Remove old entry
|
|
209
|
-
const newMetadata = { ...formData.metadata };
|
|
210
|
-
delete newMetadata[keyToEdit];
|
|
211
|
-
setFormData({ ...formData, metadata: newMetadata });
|
|
212
|
-
setMetadataInputMode("key");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
else if ((input === "d" || key.delete) &&
|
|
216
|
-
selectedMetadataIndex >= 1 &&
|
|
217
|
-
selectedMetadataIndex <= metadataKeys.length) {
|
|
218
|
-
// Delete selected item (selectedMetadataIndex - 1 gives array index)
|
|
219
|
-
const keyToDelete = metadataKeys[selectedMetadataIndex - 1];
|
|
220
|
-
const newMetadata = { ...formData.metadata };
|
|
221
|
-
delete newMetadata[keyToDelete];
|
|
222
|
-
setFormData({ ...formData, metadata: newMetadata });
|
|
223
|
-
// Stay at same position or move to add new if we deleted the last item
|
|
224
|
-
const newLength = Object.keys(newMetadata).length;
|
|
225
|
-
if (selectedMetadataIndex > newLength) {
|
|
226
|
-
setSelectedMetadataIndex(Math.max(0, newLength));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else if (key.escape || input === "q") {
|
|
230
|
-
// Exit metadata section
|
|
231
|
-
setInMetadataSection(false);
|
|
232
|
-
setSelectedMetadataIndex(0);
|
|
233
|
-
setMetadataKey("");
|
|
234
|
-
setMetadataValue("");
|
|
235
|
-
setMetadataInputMode(null);
|
|
236
|
-
}
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
// Back to list (only when not in metadata section)
|
|
142
|
+
// Back to list
|
|
240
143
|
if (input === "q" || key.escape) {
|
|
241
144
|
onBack();
|
|
242
145
|
return;
|
|
243
146
|
}
|
|
244
|
-
// Submit form
|
|
147
|
+
// Submit form with Ctrl+S
|
|
245
148
|
if (input === "s" && key.ctrl) {
|
|
246
149
|
handleCreate();
|
|
247
150
|
return;
|
|
248
151
|
}
|
|
249
|
-
//
|
|
250
|
-
if (currentField === "
|
|
152
|
+
// Enter key on metadata field to enter metadata section
|
|
153
|
+
if (currentField === "metadata" && key.return) {
|
|
154
|
+
setInMetadataSection(true);
|
|
155
|
+
setSelectedMetadataIndex(0);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Handle Enter on any field to submit
|
|
159
|
+
if (key.return) {
|
|
251
160
|
handleCreate();
|
|
252
161
|
return;
|
|
253
162
|
}
|
|
@@ -266,13 +175,96 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
266
175
|
setCurrentField(fields[currentFieldIndex + 1].key);
|
|
267
176
|
return;
|
|
268
177
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
178
|
+
}, { isActive: !inMetadataSection });
|
|
179
|
+
// Metadata section input handler - active when in metadata section
|
|
180
|
+
useInput((input, key) => {
|
|
181
|
+
const metadataKeys = Object.keys(formData.metadata);
|
|
182
|
+
const maxIndex = metadataKeys.length + 1;
|
|
183
|
+
// Handle input mode (typing key or value)
|
|
184
|
+
if (metadataInputMode) {
|
|
185
|
+
if (metadataInputMode === "key" && key.return && metadataKey.trim()) {
|
|
186
|
+
setMetadataInputMode("value");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
else if (metadataInputMode === "value" && key.return) {
|
|
190
|
+
if (metadataKey.trim() && metadataValue.trim()) {
|
|
191
|
+
setFormData({
|
|
192
|
+
...formData,
|
|
193
|
+
metadata: {
|
|
194
|
+
...formData.metadata,
|
|
195
|
+
[metadataKey.trim()]: metadataValue.trim(),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
setMetadataKey("");
|
|
200
|
+
setMetadataValue("");
|
|
201
|
+
setMetadataInputMode(null);
|
|
202
|
+
setSelectedMetadataIndex(0);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
else if (key.escape) {
|
|
206
|
+
setMetadataKey("");
|
|
207
|
+
setMetadataValue("");
|
|
208
|
+
setMetadataInputMode(null);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
else if (key.tab) {
|
|
212
|
+
setMetadataInputMode(metadataInputMode === "key" ? "value" : "key");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
273
215
|
return;
|
|
274
216
|
}
|
|
275
|
-
|
|
217
|
+
// Navigation mode in metadata section
|
|
218
|
+
if (key.upArrow && selectedMetadataIndex > 0) {
|
|
219
|
+
setSelectedMetadataIndex(selectedMetadataIndex - 1);
|
|
220
|
+
}
|
|
221
|
+
else if (key.downArrow && selectedMetadataIndex < maxIndex) {
|
|
222
|
+
setSelectedMetadataIndex(selectedMetadataIndex + 1);
|
|
223
|
+
}
|
|
224
|
+
else if (key.return) {
|
|
225
|
+
if (selectedMetadataIndex === 0) {
|
|
226
|
+
setMetadataKey("");
|
|
227
|
+
setMetadataValue("");
|
|
228
|
+
setMetadataInputMode("key");
|
|
229
|
+
}
|
|
230
|
+
else if (selectedMetadataIndex === maxIndex) {
|
|
231
|
+
setInMetadataSection(false);
|
|
232
|
+
setSelectedMetadataIndex(0);
|
|
233
|
+
setMetadataKey("");
|
|
234
|
+
setMetadataValue("");
|
|
235
|
+
setMetadataInputMode(null);
|
|
236
|
+
}
|
|
237
|
+
else if (selectedMetadataIndex >= 1 &&
|
|
238
|
+
selectedMetadataIndex <= metadataKeys.length) {
|
|
239
|
+
const keyToEdit = metadataKeys[selectedMetadataIndex - 1];
|
|
240
|
+
setMetadataKey(keyToEdit || "");
|
|
241
|
+
setMetadataValue(formData.metadata[keyToEdit] || "");
|
|
242
|
+
const newMetadata = { ...formData.metadata };
|
|
243
|
+
delete newMetadata[keyToEdit];
|
|
244
|
+
setFormData({ ...formData, metadata: newMetadata });
|
|
245
|
+
setMetadataInputMode("key");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if ((input === "d" || key.delete) &&
|
|
249
|
+
selectedMetadataIndex >= 1 &&
|
|
250
|
+
selectedMetadataIndex <= metadataKeys.length) {
|
|
251
|
+
const keyToDelete = metadataKeys[selectedMetadataIndex - 1];
|
|
252
|
+
const newMetadata = { ...formData.metadata };
|
|
253
|
+
delete newMetadata[keyToDelete];
|
|
254
|
+
setFormData({ ...formData, metadata: newMetadata });
|
|
255
|
+
const newLength = Object.keys(newMetadata).length;
|
|
256
|
+
if (selectedMetadataIndex > newLength) {
|
|
257
|
+
setSelectedMetadataIndex(Math.max(0, newLength));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (key.escape || input === "q") {
|
|
261
|
+
setInMetadataSection(false);
|
|
262
|
+
setSelectedMetadataIndex(0);
|
|
263
|
+
setMetadataKey("");
|
|
264
|
+
setMetadataValue("");
|
|
265
|
+
setMetadataInputMode(null);
|
|
266
|
+
}
|
|
267
|
+
}, { isActive: inMetadataSection });
|
|
276
268
|
// Validate custom resource configuration
|
|
277
269
|
const validateCustomResources = () => {
|
|
278
270
|
if (formData.resource_size !== "CUSTOM_SIZE") {
|
|
@@ -383,7 +375,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
383
375
|
return (_jsx(FormActionButton, { label: field.label, isActive: isActive, hint: "[Enter to create]" }, field.key));
|
|
384
376
|
}
|
|
385
377
|
if (field.type === "text") {
|
|
386
|
-
return (_jsx(FormTextInput, { label: field.label, value: String(fieldData || ""), onChange: (value) => setFormData({ ...formData, [field.key]: value }), isActive: isActive, placeholder: field.placeholder }, field.key));
|
|
378
|
+
return (_jsx(FormTextInput, { label: field.label, value: String(fieldData || ""), onChange: (value) => setFormData({ ...formData, [field.key]: value }), onSubmit: handleCreate, isActive: isActive, placeholder: field.placeholder }, field.key));
|
|
387
379
|
}
|
|
388
380
|
if (field.type === "select") {
|
|
389
381
|
const value = fieldData;
|
|
@@ -76,6 +76,7 @@ export const NetworkPolicyCreatePage = ({ onBack, onCreate, initialPolicy, }) =>
|
|
|
76
76
|
}
|
|
77
77
|
}, currentField === "allow_all");
|
|
78
78
|
const handleDevboxNav = useFormSelectNavigation(formData.allow_devbox_to_devbox, BOOLEAN_OPTIONS, (value) => setFormData({ ...formData, allow_devbox_to_devbox: value }), currentField === "allow_devbox_to_devbox");
|
|
79
|
+
// Main form input handler - active when not in hostnames expanded mode
|
|
79
80
|
useInput((input, key) => {
|
|
80
81
|
// Handle result screen
|
|
81
82
|
if (result) {
|
|
@@ -83,7 +84,9 @@ export const NetworkPolicyCreatePage = ({ onBack, onCreate, initialPolicy, }) =>
|
|
|
83
84
|
if (onCreate) {
|
|
84
85
|
onCreate(result);
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
else {
|
|
88
|
+
onBack();
|
|
89
|
+
}
|
|
87
90
|
}
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
@@ -103,10 +106,6 @@ export const NetworkPolicyCreatePage = ({ onBack, onCreate, initialPolicy, }) =>
|
|
|
103
106
|
if (submitting) {
|
|
104
107
|
return;
|
|
105
108
|
}
|
|
106
|
-
// Handle hostnames expanded mode - let FormListManager handle input
|
|
107
|
-
if (hostnamesExpanded) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
109
|
// Back to list
|
|
111
110
|
if (input === "q" || key.escape) {
|
|
112
111
|
onBack();
|
|
@@ -117,16 +116,16 @@ export const NetworkPolicyCreatePage = ({ onBack, onCreate, initialPolicy, }) =>
|
|
|
117
116
|
handleSubmit();
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
|
-
// Handle Enter on submit field
|
|
121
|
-
if (currentField === "submit" && key.return) {
|
|
122
|
-
handleSubmit();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
119
|
// Handle Enter on hostnames field to expand
|
|
126
120
|
if (currentField === "allowed_hostnames" && key.return) {
|
|
127
121
|
setHostnamesExpanded(true);
|
|
128
122
|
return;
|
|
129
123
|
}
|
|
124
|
+
// Handle Enter on any field to submit (including text/select fields)
|
|
125
|
+
if (key.return) {
|
|
126
|
+
handleSubmit();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
130
129
|
// Handle select field navigation
|
|
131
130
|
if (handleAllowAllNav(input, key))
|
|
132
131
|
return;
|
|
@@ -142,7 +141,7 @@ export const NetworkPolicyCreatePage = ({ onBack, onCreate, initialPolicy, }) =>
|
|
|
142
141
|
setCurrentField(fields[currentFieldIndex + 1].key);
|
|
143
142
|
return;
|
|
144
143
|
}
|
|
145
|
-
});
|
|
144
|
+
}, { isActive: !hostnamesExpanded });
|
|
146
145
|
const handleSubmit = async () => {
|
|
147
146
|
// Validate required fields
|
|
148
147
|
if (!formData.name.trim()) {
|
|
@@ -243,7 +242,7 @@ export const NetworkPolicyCreatePage = ({ onBack, onCreate, initialPolicy, }) =>
|
|
|
243
242
|
if (field.key === "name" && validationError) {
|
|
244
243
|
setValidationError(null);
|
|
245
244
|
}
|
|
246
|
-
}, isActive: isActive, placeholder: field.key === "name"
|
|
245
|
+
}, onSubmit: handleSubmit, isActive: isActive, placeholder: field.key === "name"
|
|
247
246
|
? "my-network-policy"
|
|
248
247
|
: field.key === "description"
|
|
249
248
|
? "Policy description"
|
|
@@ -3,6 +3,6 @@ import { Text } from "ink";
|
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import { FormField } from "./FormField.js";
|
|
5
5
|
import { colors } from "../../utils/theme.js";
|
|
6
|
-
export const FormTextInput = ({ label, value, onChange, isActive, placeholder, error, }) => {
|
|
7
|
-
return (_jsx(FormField, { label: label, isActive: isActive, error: error, children: isActive ? (_jsx(TextInput, { value: value, onChange: onChange, placeholder: placeholder })) : (_jsx(Text, { color: error ? colors.error : colors.text, children: value || "(empty)" })) }));
|
|
6
|
+
export const FormTextInput = ({ label, value, onChange, isActive, placeholder, error, onSubmit, }) => {
|
|
7
|
+
return (_jsx(FormField, { label: label, isActive: isActive, error: error, children: isActive ? (_jsx(TextInput, { value: value, onChange: onChange, placeholder: placeholder, onSubmit: onSubmit })) : (_jsx(Text, { color: error ? colors.error : colors.text, children: value || "(empty)" })) }));
|
|
8
8
|
};
|
|
@@ -51,8 +51,8 @@ export function BlueprintDetailScreen({ blueprintId, }) {
|
|
|
51
51
|
}, [blueprintId, loading, fetchedBlueprint]);
|
|
52
52
|
// Use fetched blueprint for full details, fall back to store for basic display
|
|
53
53
|
const blueprint = fetchedBlueprint || blueprintFromStore;
|
|
54
|
-
// Show loading state while fetching
|
|
55
|
-
if (
|
|
54
|
+
// Show loading state while fetching or before fetch starts
|
|
55
|
+
if (!blueprint && blueprintId && !error) {
|
|
56
56
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
57
57
|
{ label: "Blueprints" },
|
|
58
58
|
{ label: "Loading...", active: true },
|
|
@@ -183,6 +183,21 @@ export function BlueprintDetailScreen({ blueprintId, }) {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
// Error section - show failure reason if present
|
|
187
|
+
if (blueprint.failure_reason) {
|
|
188
|
+
detailSections.push({
|
|
189
|
+
title: "Error",
|
|
190
|
+
icon: figures.cross,
|
|
191
|
+
color: colors.error,
|
|
192
|
+
fields: [
|
|
193
|
+
{
|
|
194
|
+
label: "Failure Reason",
|
|
195
|
+
value: blueprint.failure_reason,
|
|
196
|
+
color: colors.error,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
186
201
|
// Operations available for blueprints
|
|
187
202
|
const operations = [
|
|
188
203
|
{
|
|
@@ -262,6 +277,9 @@ export function BlueprintDetailScreen({ blueprintId, }) {
|
|
|
262
277
|
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", bp.id] }, "core-id"));
|
|
263
278
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", bp.name || "(none)"] }, "core-name"));
|
|
264
279
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", bp.status] }, "core-status"));
|
|
280
|
+
if (bp.failure_reason) {
|
|
281
|
+
lines.push(_jsxs(Text, { color: colors.error, children: [" ", "Failure Reason: ", bp.failure_reason] }, "core-failure"));
|
|
282
|
+
}
|
|
265
283
|
if (bp.create_time_ms) {
|
|
266
284
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(bp.create_time_ms).toLocaleString()] }, "core-created"));
|
|
267
285
|
}
|
|
@@ -40,16 +40,16 @@ export function DevboxDetailScreen({ devboxId }) {
|
|
|
40
40
|
}, [devboxFromStore, devboxId, loading, fetchedDevbox, setDevboxesInStore]);
|
|
41
41
|
// Use devbox from store or fetched devbox
|
|
42
42
|
const devbox = devboxFromStore || fetchedDevbox;
|
|
43
|
-
// Show loading state while fetching
|
|
44
|
-
if (
|
|
43
|
+
// Show loading state while fetching or before fetch starts
|
|
44
|
+
if (!devbox && devboxId && !error) {
|
|
45
45
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Loading...", active: true }] }), _jsx(SpinnerComponent, { message: "Loading devbox details..." })] }));
|
|
46
46
|
}
|
|
47
47
|
// Show error state if fetch failed
|
|
48
48
|
if (error) {
|
|
49
49
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Error", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load devbox details", error: error })] }));
|
|
50
50
|
}
|
|
51
|
-
// Show error if no devbox found
|
|
52
|
-
if (!devbox
|
|
51
|
+
// Show error if no devbox found
|
|
52
|
+
if (!devbox) {
|
|
53
53
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Not Found", active: true }] }), _jsx(ErrorMessage, { message: `Devbox ${devboxId || "unknown"} not found`, error: new Error("Devbox not found in cache and could not be fetched") })] }));
|
|
54
54
|
}
|
|
55
55
|
// At this point devbox is guaranteed to exist (loading check above handles the null case)
|
|
@@ -58,8 +58,8 @@ export function NetworkPolicyDetailScreen({ networkPolicyId, }) {
|
|
|
58
58
|
}, [networkPolicyId, loading, fetchedPolicy]);
|
|
59
59
|
// Use fetched policy for full details, fall back to store for basic display
|
|
60
60
|
const policy = fetchedPolicy || policyFromStore;
|
|
61
|
-
// Show loading state while fetching
|
|
62
|
-
if (
|
|
61
|
+
// Show loading state while fetching or before fetch starts
|
|
62
|
+
if (!policy && networkPolicyId && !error) {
|
|
63
63
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
64
64
|
{ label: "Network Policies" },
|
|
65
65
|
{ label: "Loading...", active: true },
|
|
@@ -111,8 +111,8 @@ export function ObjectDetailScreen({ objectId }) {
|
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
}, { isActive: showDownloadPrompt || !!downloadResult || !!downloadError });
|
|
114
|
-
// Show loading state while fetching
|
|
115
|
-
if (
|
|
114
|
+
// Show loading state while fetching or before fetch starts
|
|
115
|
+
if (!storageObject && objectId && !error) {
|
|
116
116
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
117
117
|
{ label: "Storage Objects" },
|
|
118
118
|
{ label: "Loading...", active: true },
|
|
@@ -50,8 +50,8 @@ export function SnapshotDetailScreen({ snapshotId, }) {
|
|
|
50
50
|
}, [snapshotId, loading, fetchedSnapshot]);
|
|
51
51
|
// Use fetched snapshot for full details, fall back to store for basic display
|
|
52
52
|
const snapshot = fetchedSnapshot || snapshotFromStore;
|
|
53
|
-
// Show loading state while fetching
|
|
54
|
-
if (
|
|
53
|
+
// Show loading state while fetching or before fetch starts
|
|
54
|
+
if (!snapshot && snapshotId && !error) {
|
|
55
55
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
56
56
|
{ label: "Snapshots" },
|
|
57
57
|
{ label: "Loading...", active: true },
|
package/dist/utils/commands.js
CHANGED
|
@@ -312,6 +312,24 @@ export function createProgram() {
|
|
|
312
312
|
const { pruneBlueprints } = await import("../commands/blueprint/prune.js");
|
|
313
313
|
await pruneBlueprints(name, options);
|
|
314
314
|
});
|
|
315
|
+
blueprint
|
|
316
|
+
.command("from-dockerfile")
|
|
317
|
+
.description("Create a blueprint from a Dockerfile with build context support")
|
|
318
|
+
.requiredOption("--name <name>", "Blueprint name (required)")
|
|
319
|
+
.option("--build-context <path>", "Build context directory (default: current directory)")
|
|
320
|
+
.option("--dockerfile <path>", "Dockerfile path (default: Dockerfile in build context)")
|
|
321
|
+
.option("--system-setup-commands <commands...>", "System setup commands")
|
|
322
|
+
.option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
|
|
323
|
+
.option("--architecture <arch>", "Architecture (arm64, x86_64)")
|
|
324
|
+
.option("--available-ports <ports...>", "Available ports")
|
|
325
|
+
.option("--root", "Run as root")
|
|
326
|
+
.option("--user <user:uid>", "Run as this user (format: username:uid)")
|
|
327
|
+
.option("--ttl <seconds>", "TTL in seconds for the build context object (default: 3600)")
|
|
328
|
+
.option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
|
|
329
|
+
.action(async (options) => {
|
|
330
|
+
const { createBlueprintFromDockerfile } = await import("../commands/blueprint/from-dockerfile.js");
|
|
331
|
+
await createBlueprintFromDockerfile(options);
|
|
332
|
+
});
|
|
315
333
|
// Object storage commands
|
|
316
334
|
const object = program
|
|
317
335
|
.command("object")
|