@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.
@@ -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
- onBack();
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 (loading && !blueprint) {
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 (loading) {
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 and not loading
52
- if (!devbox && !loading) {
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 (loading && !policy) {
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 (loading && !storageObject) {
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 (loading && !snapshot) {
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 },
@@ -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
- const time = date.toLocaleTimeString("en-US", {
23
- hour12: false,
24
- hour: "2-digit",
25
- minute: "2-digit",
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
- const monthDay = date.toLocaleDateString("en-US", {
34
- month: "short",
35
- day: "numeric",
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
- const fullDate = date.toLocaleDateString("en-US", {
41
- year: "numeric",
42
- month: "short",
43
- day: "numeric",
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runloop/rl-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Beautiful CLI for the Runloop platform",
5
5
  "type": "module",
6
6
  "bin": {