@runloop/rl-cli 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/commands/blueprint/from-dockerfile.js +182 -0
- package/dist/components/Banner.js +27 -5
- package/dist/components/DevboxCreatePage.js +103 -111
- package/dist/components/LogsViewer.js +140 -61
- package/dist/components/MainMenu.js +77 -22
- package/dist/components/NavigationTips.js +174 -4
- package/dist/components/NetworkPolicyCreatePage.js +11 -12
- package/dist/components/ResourceDetailPage.js +44 -2
- 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/dist/utils/logFormatter.js +16 -17
- package/package.json +1 -1
|
@@ -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"
|
|
@@ -50,6 +50,43 @@ export function ResourceDetailPage({ resource: initialResource, resourceType, ge
|
|
|
50
50
|
}, []);
|
|
51
51
|
// Local state for resource data (updated by polling)
|
|
52
52
|
const [currentResource, setCurrentResource] = React.useState(initialResource);
|
|
53
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
54
|
+
// Copy to clipboard helper
|
|
55
|
+
const copyToClipboard = React.useCallback(async (text) => {
|
|
56
|
+
const { spawn } = await import("child_process");
|
|
57
|
+
const platform = process.platform;
|
|
58
|
+
let command;
|
|
59
|
+
let args;
|
|
60
|
+
if (platform === "darwin") {
|
|
61
|
+
command = "pbcopy";
|
|
62
|
+
args = [];
|
|
63
|
+
}
|
|
64
|
+
else if (platform === "win32") {
|
|
65
|
+
command = "clip";
|
|
66
|
+
args = [];
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
command = "xclip";
|
|
70
|
+
args = ["-selection", "clipboard"];
|
|
71
|
+
}
|
|
72
|
+
const proc = spawn(command, args);
|
|
73
|
+
proc.stdin.write(text);
|
|
74
|
+
proc.stdin.end();
|
|
75
|
+
proc.on("exit", (code) => {
|
|
76
|
+
if (code === 0) {
|
|
77
|
+
setCopyStatus("Copied ID to clipboard!");
|
|
78
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setCopyStatus("Failed to copy");
|
|
82
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
proc.on("error", () => {
|
|
86
|
+
setCopyStatus("Copy not supported");
|
|
87
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
88
|
+
});
|
|
89
|
+
}, []);
|
|
53
90
|
const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
|
|
54
91
|
const [detailScroll, setDetailScroll] = React.useState(0);
|
|
55
92
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
@@ -106,6 +143,10 @@ export function ResourceDetailPage({ resource: initialResource, resourceType, ge
|
|
|
106
143
|
if (input === "q" || key.escape) {
|
|
107
144
|
onBack();
|
|
108
145
|
}
|
|
146
|
+
else if (input === "c") {
|
|
147
|
+
// Copy resource ID to clipboard
|
|
148
|
+
copyToClipboard(getId(currentResource));
|
|
149
|
+
}
|
|
109
150
|
else if (input === "i" && buildDetailLines) {
|
|
110
151
|
setShowDetailedInfo(true);
|
|
111
152
|
setDetailScroll(0);
|
|
@@ -176,11 +217,12 @@ export function ResourceDetailPage({ resource: initialResource, resourceType, ge
|
|
|
176
217
|
.map((field, fieldIndex) => (_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, children: [field.label, " "] }), typeof field.value === "string" ? (_jsx(Text, { color: field.color, dimColor: !field.color, children: field.value })) : (field.value)] }, fieldIndex))) })] }, sectionIndex))), additionalContent, operations.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: operations.map((op, index) => {
|
|
177
218
|
const isSelected = index === selectedOperation;
|
|
178
219
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
179
|
-
}) })] })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
220
|
+
}) })] })), copyStatus && (_jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.success, bold: true, children: copyStatus }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
180
221
|
{ key: "Enter", label: "Execute" },
|
|
222
|
+
{ key: "c", label: "Copy ID" },
|
|
181
223
|
{ key: "i", label: "Full Details", condition: !!buildDetailLines },
|
|
182
224
|
{ key: "o", label: "Browser", condition: !!getUrl },
|
|
183
|
-
{ key: "q", label: "Back" },
|
|
225
|
+
{ key: "q/Ctrl+C", label: "Back/Quit" },
|
|
184
226
|
] })] }));
|
|
185
227
|
}
|
|
186
228
|
// Helper to format timestamp as "time (ago)"
|
|
@@ -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")
|
|
@@ -13,36 +13,35 @@ const SOURCE_CONFIG = {
|
|
|
13
13
|
const SOURCE_WIDTH = 5;
|
|
14
14
|
/**
|
|
15
15
|
* Format timestamp based on how recent the log is
|
|
16
|
+
* Always returns a fixed-width string for consistent alignment
|
|
16
17
|
*/
|
|
17
18
|
export function formatTimestamp(timestampMs) {
|
|
18
19
|
const date = new Date(timestampMs);
|
|
19
20
|
const now = new Date();
|
|
20
21
|
const isToday = date.toDateString() === now.toDateString();
|
|
21
22
|
const isThisYear = date.getFullYear() === now.getFullYear();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
second: "2-digit",
|
|
27
|
-
});
|
|
23
|
+
// Build time components manually for consistent formatting
|
|
24
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
25
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
26
|
+
const seconds = date.getSeconds().toString().padStart(2, "0");
|
|
28
27
|
const ms = date.getMilliseconds().toString().padStart(3, "0");
|
|
28
|
+
const time = `${hours}:${minutes}:${seconds}`;
|
|
29
29
|
if (isToday) {
|
|
30
|
+
// Format: "HH:MM:SS.mmm" (12 chars)
|
|
30
31
|
return `${time}.${ms}`;
|
|
31
32
|
}
|
|
32
33
|
else if (isThisYear) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
return `${monthDay} ${time}`;
|
|
34
|
+
// Format: "Mon DD HH:MM:SS" (15 chars, pad day to 2)
|
|
35
|
+
const month = date.toLocaleDateString("en-US", { month: "short" });
|
|
36
|
+
const day = date.getDate().toString().padStart(2, " ");
|
|
37
|
+
return `${month} ${day} ${time}`;
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
return `${fullDate} ${time}`;
|
|
40
|
+
// Format: "YYYY Mon DD HH:MM:SS" (20 chars)
|
|
41
|
+
const year = date.getFullYear();
|
|
42
|
+
const month = date.toLocaleDateString("en-US", { month: "short" });
|
|
43
|
+
const day = date.getDate().toString().padStart(2, " ");
|
|
44
|
+
return `${year} ${month} ${day} ${time}`;
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
/**
|