@runloop/rl-cli 0.0.2 → 0.1.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.
Files changed (73) hide show
  1. package/README.md +64 -29
  2. package/dist/cli.js +420 -76
  3. package/dist/commands/auth.js +12 -10
  4. package/dist/commands/blueprint/create.js +108 -0
  5. package/dist/commands/blueprint/get.js +37 -0
  6. package/dist/commands/blueprint/list.js +303 -224
  7. package/dist/commands/blueprint/logs.js +40 -0
  8. package/dist/commands/blueprint/preview.js +45 -0
  9. package/dist/commands/devbox/create.js +10 -9
  10. package/dist/commands/devbox/delete.js +8 -8
  11. package/dist/commands/devbox/download.js +49 -0
  12. package/dist/commands/devbox/exec.js +23 -13
  13. package/dist/commands/devbox/execAsync.js +43 -0
  14. package/dist/commands/devbox/get.js +37 -0
  15. package/dist/commands/devbox/getAsync.js +37 -0
  16. package/dist/commands/devbox/list.js +390 -205
  17. package/dist/commands/devbox/logs.js +40 -0
  18. package/dist/commands/devbox/read.js +49 -0
  19. package/dist/commands/devbox/resume.js +37 -0
  20. package/dist/commands/devbox/rsync.js +118 -0
  21. package/dist/commands/devbox/scp.js +122 -0
  22. package/dist/commands/devbox/shutdown.js +37 -0
  23. package/dist/commands/devbox/ssh.js +104 -0
  24. package/dist/commands/devbox/suspend.js +37 -0
  25. package/dist/commands/devbox/tunnel.js +120 -0
  26. package/dist/commands/devbox/upload.js +10 -10
  27. package/dist/commands/devbox/write.js +51 -0
  28. package/dist/commands/mcp-http.js +37 -0
  29. package/dist/commands/mcp-install.js +120 -0
  30. package/dist/commands/mcp.js +30 -0
  31. package/dist/commands/menu.js +70 -0
  32. package/dist/commands/object/delete.js +37 -0
  33. package/dist/commands/object/download.js +88 -0
  34. package/dist/commands/object/get.js +37 -0
  35. package/dist/commands/object/list.js +112 -0
  36. package/dist/commands/object/upload.js +130 -0
  37. package/dist/commands/snapshot/create.js +12 -11
  38. package/dist/commands/snapshot/delete.js +8 -8
  39. package/dist/commands/snapshot/list.js +59 -91
  40. package/dist/commands/snapshot/status.js +37 -0
  41. package/dist/components/ActionsPopup.js +16 -13
  42. package/dist/components/Banner.js +5 -8
  43. package/dist/components/Breadcrumb.js +6 -6
  44. package/dist/components/DetailView.js +7 -4
  45. package/dist/components/DevboxActionsMenu.js +347 -189
  46. package/dist/components/DevboxCard.js +15 -14
  47. package/dist/components/DevboxCreatePage.js +147 -113
  48. package/dist/components/DevboxDetailPage.js +182 -103
  49. package/dist/components/ErrorMessage.js +5 -4
  50. package/dist/components/Header.js +4 -3
  51. package/dist/components/MainMenu.js +72 -0
  52. package/dist/components/MetadataDisplay.js +17 -9
  53. package/dist/components/OperationsMenu.js +6 -5
  54. package/dist/components/ResourceActionsMenu.js +117 -0
  55. package/dist/components/ResourceListView.js +213 -0
  56. package/dist/components/Spinner.js +5 -4
  57. package/dist/components/StatusBadge.js +81 -31
  58. package/dist/components/SuccessMessage.js +4 -3
  59. package/dist/components/Table.example.js +53 -23
  60. package/dist/components/Table.js +19 -11
  61. package/dist/hooks/useCursorPagination.js +125 -0
  62. package/dist/mcp/server-http.js +416 -0
  63. package/dist/mcp/server.js +397 -0
  64. package/dist/utils/CommandExecutor.js +22 -6
  65. package/dist/utils/client.js +20 -3
  66. package/dist/utils/config.js +40 -4
  67. package/dist/utils/interactiveCommand.js +14 -0
  68. package/dist/utils/output.js +17 -17
  69. package/dist/utils/ssh.js +160 -0
  70. package/dist/utils/sshSession.js +29 -0
  71. package/dist/utils/theme.js +22 -0
  72. package/dist/utils/url.js +39 -0
  73. package/package.json +29 -4
@@ -1,150 +1,105 @@
1
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, useStdout, useApp } 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 '../../components/Header.js';
8
- import { SpinnerComponent } from '../../components/Spinner.js';
9
- import { ErrorMessage } from '../../components/ErrorMessage.js';
10
- import { SuccessMessage } from '../../components/SuccessMessage.js';
11
- import { StatusBadge } from '../../components/StatusBadge.js';
12
- import { Breadcrumb } from '../../components/Breadcrumb.js';
13
- import { MetadataDisplay } from '../../components/MetadataDisplay.js';
14
- import { Table, createTextColumn, createComponentColumn } from '../../components/Table.js';
15
- import { OperationsMenu } from '../../components/OperationsMenu.js';
16
- import { createExecutor } from '../../utils/CommandExecutor.js';
2
+ import React from "react";
3
+ import { Box, Text, useInput, useStdout } 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 "../../components/Header.js";
8
+ import { SpinnerComponent } from "../../components/Spinner.js";
9
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
10
+ import { SuccessMessage } from "../../components/SuccessMessage.js";
11
+ import { Breadcrumb } from "../../components/Breadcrumb.js";
12
+ import { createTextColumn, Table } from "../../components/Table.js";
13
+ import { ActionsPopup } from "../../components/ActionsPopup.js";
14
+ import { formatTimeAgo } from "../../components/ResourceListView.js";
15
+ import { createExecutor } from "../../utils/CommandExecutor.js";
16
+ import { getBlueprintUrl } from "../../utils/url.js";
17
+ import { colors } from "../../utils/theme.js";
18
+ import { getStatusDisplay } from "../../components/StatusBadge.js";
19
+ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
17
20
  const PAGE_SIZE = 10;
18
21
  const MAX_FETCH = 100;
19
- // Format time ago
20
- const formatTimeAgo = (timestamp) => {
21
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
22
- if (seconds < 60)
23
- return `${seconds}s ago`;
24
- const minutes = Math.floor(seconds / 60);
25
- if (minutes < 60)
26
- return `${minutes}m ago`;
27
- const hours = Math.floor(minutes / 60);
28
- if (hours < 24)
29
- return `${hours}h ago`;
30
- const days = Math.floor(hours / 24);
31
- if (days < 30)
32
- return `${days}d ago`;
33
- const months = Math.floor(days / 30);
34
- if (months < 12)
35
- return `${months}mo ago`;
36
- const years = Math.floor(months / 12);
37
- return `${years}y ago`;
38
- };
39
- const ListBlueprintsUI = () => {
22
+ const ListBlueprintsUI = ({ onBack, onExit }) => {
40
23
  const { stdout } = useStdout();
41
- const { exit } = useApp();
42
- const [loading, setLoading] = React.useState(true);
43
- const [blueprints, setBlueprints] = React.useState([]);
44
- const [error, setError] = React.useState(null);
45
- const [currentPage, setCurrentPage] = React.useState(0);
46
- const [selectedIndex, setSelectedIndex] = React.useState(0);
47
- const [showDetails, setShowDetails] = React.useState(false);
24
+ const [selectedBlueprint, setSelectedBlueprint] = React.useState(null);
48
25
  const [selectedOperation, setSelectedOperation] = React.useState(0);
49
26
  const [executingOperation, setExecutingOperation] = React.useState(null);
50
- const [operationInput, setOperationInput] = React.useState('');
27
+ const [operationInput, setOperationInput] = React.useState("");
51
28
  const [operationResult, setOperationResult] = React.useState(null);
52
29
  const [operationError, setOperationError] = React.useState(null);
30
+ const [loading, setLoading] = React.useState(false);
31
+ const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
32
+ // List view state - moved to top to ensure hooks are called in same order
33
+ const [blueprints, setBlueprints] = React.useState([]);
34
+ const [listError, setListError] = React.useState(null);
35
+ const [currentPage, setCurrentPage] = React.useState(0);
36
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
37
+ const [showActions, setShowActions] = React.useState(false);
38
+ const [showPopup, setShowPopup] = React.useState(false);
53
39
  // Calculate responsive column widths
54
40
  const terminalWidth = stdout?.columns || 120;
55
41
  const showDescription = terminalWidth >= 120;
56
- const showFullId = terminalWidth >= 80;
42
+ const statusIconWidth = 2;
43
+ const statusTextWidth = 10;
57
44
  const idWidth = 25;
58
45
  const nameWidth = terminalWidth >= 120 ? 30 : 25;
59
46
  const descriptionWidth = 40;
60
47
  const timeWidth = 20;
61
- const allOperations = [
62
- {
63
- key: 'create_devbox',
64
- label: 'Create Devbox from Blueprint',
65
- color: 'green',
66
- icon: figures.play,
67
- needsInput: true,
68
- inputPrompt: 'Devbox name (optional):',
69
- inputPlaceholder: 'my-devbox',
70
- },
71
- {
72
- key: 'delete',
73
- label: 'Delete Blueprint',
74
- color: 'red',
48
+ // Helper function to generate operations based on selected blueprint
49
+ const getOperationsForBlueprint = (blueprint) => {
50
+ const operations = [];
51
+ // Only show create devbox option if blueprint is successfully built
52
+ if (blueprint &&
53
+ (blueprint.status === "build_complete" ||
54
+ blueprint.status === "building_complete")) {
55
+ operations.push({
56
+ key: "create_devbox",
57
+ label: "Create Devbox from Blueprint",
58
+ color: colors.success,
59
+ icon: figures.play,
60
+ });
61
+ }
62
+ // Always show delete option
63
+ operations.push({
64
+ key: "delete",
65
+ label: "Delete Blueprint",
66
+ color: colors.error,
75
67
  icon: figures.cross,
76
- },
77
- ];
68
+ });
69
+ return operations;
70
+ };
71
+ // Fetch blueprints - moved to top to ensure hooks are called in same order
78
72
  React.useEffect(() => {
79
- const list = async () => {
73
+ const fetchBlueprints = async () => {
80
74
  try {
75
+ setLoading(true);
81
76
  const client = getClient();
82
77
  const allBlueprints = [];
83
78
  let count = 0;
84
79
  for await (const blueprint of client.blueprints.list()) {
85
80
  allBlueprints.push(blueprint);
86
81
  count++;
87
- if (count >= MAX_FETCH) {
82
+ if (count >= MAX_FETCH)
88
83
  break;
89
- }
90
84
  }
91
85
  setBlueprints(allBlueprints);
92
86
  }
93
87
  catch (err) {
94
- setError(err);
88
+ setListError(err);
95
89
  }
96
90
  finally {
97
91
  setLoading(false);
98
92
  }
99
93
  };
100
- list();
94
+ fetchBlueprints();
101
95
  }, []);
102
- // Clear console when transitioning to detail view
103
- const prevShowDetailsRef = React.useRef(showDetails);
104
- React.useEffect(() => {
105
- if (showDetails && !prevShowDetailsRef.current) {
106
- console.clear();
107
- }
108
- prevShowDetailsRef.current = showDetails;
109
- }, [showDetails]);
110
- // Auto-execute operations that don't need input
111
- React.useEffect(() => {
112
- if (executingOperation === 'delete' && !loading && selectedBlueprint) {
113
- executeOperation();
114
- }
115
- }, [executingOperation]);
116
- const executeOperation = async () => {
117
- const client = getClient();
118
- const blueprint = selectedBlueprint;
119
- try {
120
- setLoading(true);
121
- switch (executingOperation) {
122
- case 'create_devbox':
123
- const devbox = await client.devboxes.create({
124
- blueprint_id: blueprint.id,
125
- name: operationInput || undefined,
126
- });
127
- setOperationResult(`Devbox created successfully!\n\n` +
128
- `ID: ${devbox.id}\n` +
129
- `Name: ${devbox.name || '(none)'}\n` +
130
- `Status: ${devbox.status}\n\n` +
131
- `Use 'rln devbox list' to view all devboxes.`);
132
- break;
133
- case 'delete':
134
- await client.blueprints.delete(blueprint.id);
135
- setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
136
- break;
137
- }
138
- }
139
- catch (err) {
140
- setOperationError(err);
141
- }
142
- finally {
143
- setLoading(false);
144
- }
145
- };
96
+ // Handle input for all views - combined into single hook
146
97
  useInput((input, key) => {
147
- const pageBlueprints = currentBlueprints.length;
98
+ // Handle Ctrl+C to force exit
99
+ if (key.ctrl && input === "c") {
100
+ process.stdout.write("\x1b[?1049l"); // Exit alternate screen
101
+ process.exit(130);
102
+ }
148
103
  // Handle operation input mode
149
104
  if (executingOperation && !operationResult && !operationError) {
150
105
  const currentOp = allOperations.find((op) => op.key === executingOperation);
@@ -152,95 +107,130 @@ const ListBlueprintsUI = () => {
152
107
  if (key.return) {
153
108
  executeOperation();
154
109
  }
155
- else if (input === 'q' || key.escape) {
110
+ else if (input === "q" || key.escape) {
156
111
  console.clear();
157
112
  setExecutingOperation(null);
158
- setOperationInput('');
113
+ setOperationInput("");
159
114
  }
160
115
  }
161
116
  return;
162
117
  }
163
118
  // Handle operation result display
164
119
  if (operationResult || operationError) {
165
- if (input === 'q' || key.escape || key.return) {
120
+ if (input === "q" || key.escape || key.return) {
166
121
  console.clear();
167
122
  setOperationResult(null);
168
123
  setOperationError(null);
169
124
  setExecutingOperation(null);
170
- setOperationInput('');
125
+ setOperationInput("");
171
126
  }
172
127
  return;
173
128
  }
174
- // Handle details view
175
- if (showDetails) {
176
- if (input === 'q' || key.escape) {
177
- console.clear();
178
- setShowDetails(false);
179
- setSelectedOperation(0);
180
- }
181
- else if (input === 'o' && selectedBlueprint) {
182
- // Open in browser
183
- const url = `https://platform.runloop.ai/blueprints/${selectedBlueprint.id}`;
184
- const openBrowser = async () => {
185
- const { exec } = await import('child_process');
186
- const platform = process.platform;
187
- let openCommand;
188
- if (platform === 'darwin') {
189
- openCommand = `open "${url}"`;
190
- }
191
- else if (platform === 'win32') {
192
- openCommand = `start "${url}"`;
193
- }
194
- else {
195
- openCommand = `xdg-open "${url}"`;
196
- }
197
- exec(openCommand);
198
- };
199
- openBrowser();
200
- }
201
- else if (key.upArrow && selectedOperation > 0) {
129
+ // Handle create devbox view
130
+ if (showCreateDevbox) {
131
+ return; // Let DevboxCreatePage handle its own input
132
+ }
133
+ // Handle actions popup overlay: consume keys and prevent table nav
134
+ if (showPopup) {
135
+ if (key.upArrow && selectedOperation > 0) {
202
136
  setSelectedOperation(selectedOperation - 1);
203
137
  }
204
- else if (key.downArrow && selectedOperation < operations.length - 1) {
138
+ else if (key.downArrow &&
139
+ selectedOperation < allOperations.length - 1) {
205
140
  setSelectedOperation(selectedOperation + 1);
206
141
  }
207
142
  else if (key.return) {
208
143
  console.clear();
209
- const op = operations[selectedOperation].key;
210
- setExecutingOperation(op);
144
+ setShowPopup(false);
145
+ const operationKey = allOperations[selectedOperation].key;
146
+ if (operationKey === "create_devbox") {
147
+ // Go directly to create devbox screen
148
+ setSelectedBlueprint(selectedBlueprintItem);
149
+ setShowCreateDevbox(true);
150
+ }
151
+ else {
152
+ // Execute other operations normally
153
+ setSelectedBlueprint(selectedBlueprintItem);
154
+ setExecutingOperation(operationKey);
155
+ executeOperation();
156
+ }
157
+ }
158
+ else if (key.escape || input === "q") {
159
+ console.clear();
160
+ setShowPopup(false);
161
+ setSelectedOperation(0);
162
+ }
163
+ else if (input === "c") {
164
+ // Create devbox hotkey - only if blueprint is complete
165
+ if (selectedBlueprintItem &&
166
+ (selectedBlueprintItem.status === "build_complete" ||
167
+ selectedBlueprintItem.status === "building_complete")) {
168
+ console.clear();
169
+ setShowPopup(false);
170
+ setSelectedBlueprint(selectedBlueprintItem);
171
+ setShowCreateDevbox(true);
172
+ }
173
+ }
174
+ else if (input === "d") {
175
+ // Delete hotkey
176
+ const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
177
+ if (deleteIndex >= 0) {
178
+ console.clear();
179
+ setShowPopup(false);
180
+ setSelectedBlueprint(selectedBlueprintItem);
181
+ setExecutingOperation("delete");
182
+ executeOperation();
183
+ }
184
+ }
185
+ return; // prevent falling through to list nav
186
+ }
187
+ // Handle actions view
188
+ if (showActions) {
189
+ if (input === "q" || key.escape) {
190
+ console.clear();
191
+ setShowActions(false);
192
+ setSelectedOperation(0);
211
193
  }
212
194
  return;
213
195
  }
214
- // Handle list view
196
+ // Handle list navigation (default view)
197
+ const pageSize = PAGE_SIZE;
198
+ const totalPages = Math.ceil(blueprints.length / pageSize);
199
+ const startIndex = currentPage * pageSize;
200
+ const endIndex = Math.min(startIndex + pageSize, blueprints.length);
201
+ const currentBlueprints = blueprints.slice(startIndex, endIndex);
202
+ const pageBlueprints = currentBlueprints.length;
215
203
  if (key.upArrow && selectedIndex > 0) {
216
204
  setSelectedIndex(selectedIndex - 1);
217
205
  }
218
206
  else if (key.downArrow && selectedIndex < pageBlueprints - 1) {
219
207
  setSelectedIndex(selectedIndex + 1);
220
208
  }
221
- else if ((input === 'n' || key.rightArrow) && currentPage < totalPages - 1) {
209
+ else if ((input === "n" || key.rightArrow) &&
210
+ currentPage < totalPages - 1) {
222
211
  setCurrentPage(currentPage + 1);
223
212
  setSelectedIndex(0);
224
213
  }
225
- else if ((input === 'p' || key.leftArrow) && currentPage > 0) {
214
+ else if ((input === "p" || key.leftArrow) && currentPage > 0) {
226
215
  setCurrentPage(currentPage - 1);
227
216
  setSelectedIndex(0);
228
217
  }
229
- else if (key.return) {
218
+ else if (input === "a") {
230
219
  console.clear();
231
- setShowDetails(true);
220
+ setShowPopup(true);
221
+ setSelectedOperation(0);
232
222
  }
233
- else if (input === 'o' && selectedBlueprint) {
223
+ else if (input === "o" && currentBlueprints[selectedIndex]) {
234
224
  // Open in browser
235
- const url = `https://platform.runloop.ai/blueprints/${selectedBlueprint.id}`;
225
+ const url = getBlueprintUrl(currentBlueprints[selectedIndex].id);
236
226
  const openBrowser = async () => {
237
- const { exec } = await import('child_process');
227
+ const { exec } = await import("child_process");
238
228
  const platform = process.platform;
239
229
  let openCommand;
240
- if (platform === 'darwin') {
230
+ if (platform === "darwin") {
241
231
  openCommand = `open "${url}"`;
242
232
  }
243
- else if (platform === 'win32') {
233
+ else if (platform === "win32") {
244
234
  openCommand = `start "${url}"`;
245
235
  }
246
236
  else {
@@ -250,25 +240,65 @@ const ListBlueprintsUI = () => {
250
240
  };
251
241
  openBrowser();
252
242
  }
253
- else if (input === 'q') {
254
- process.exit(0);
243
+ else if (key.escape) {
244
+ if (onBack) {
245
+ onBack();
246
+ }
247
+ else if (onExit) {
248
+ onExit();
249
+ }
255
250
  }
256
251
  });
257
- const totalPages = Math.ceil(blueprints.length / PAGE_SIZE);
258
- const startIndex = currentPage * PAGE_SIZE;
259
- const endIndex = Math.min(startIndex + PAGE_SIZE, blueprints.length);
252
+ // Pagination computed early to allow hooks before any returns
253
+ const pageSize = PAGE_SIZE;
254
+ const totalPages = Math.ceil(blueprints.length / pageSize);
255
+ const startIndex = currentPage * pageSize;
256
+ const endIndex = Math.min(startIndex + pageSize, blueprints.length);
260
257
  const currentBlueprints = blueprints.slice(startIndex, endIndex);
261
- const selectedBlueprint = currentBlueprints[selectedIndex];
262
- const buildComplete = blueprints.filter((b) => b.status === 'build_complete').length;
263
- const building = blueprints.filter((b) => ['provisioning', 'building'].includes(b.status)).length;
264
- const failed = blueprints.filter((b) => b.status === 'build_failed').length;
258
+ // Ensure selected index is within bounds - place before any returns
259
+ React.useEffect(() => {
260
+ if (currentBlueprints.length > 0 &&
261
+ selectedIndex >= currentBlueprints.length) {
262
+ setSelectedIndex(Math.max(0, currentBlueprints.length - 1));
263
+ }
264
+ }, [currentBlueprints.length, selectedIndex]);
265
+ const selectedBlueprintItem = currentBlueprints[selectedIndex];
266
+ // Generate operations based on selected blueprint status
267
+ const allOperations = getOperationsForBlueprint(selectedBlueprintItem);
268
+ const executeOperation = async () => {
269
+ const client = getClient();
270
+ const blueprint = selectedBlueprint;
271
+ if (!blueprint)
272
+ return;
273
+ try {
274
+ setLoading(true);
275
+ switch (executingOperation) {
276
+ case "create_devbox":
277
+ // Navigate to create devbox screen with blueprint pre-filled
278
+ setShowCreateDevbox(true);
279
+ setExecutingOperation(null);
280
+ setLoading(false);
281
+ return;
282
+ case "delete":
283
+ await client.blueprints.delete(blueprint.id);
284
+ setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
285
+ break;
286
+ }
287
+ }
288
+ catch (err) {
289
+ setOperationError(err);
290
+ }
291
+ finally {
292
+ setLoading(false);
293
+ }
294
+ };
265
295
  // Filter operations based on blueprint status
266
296
  const operations = selectedBlueprint
267
297
  ? allOperations.filter((op) => {
268
298
  const status = selectedBlueprint.status;
269
299
  // Only allow creating devbox if build is complete
270
- if (op.key === 'create_devbox') {
271
- return status === 'build_complete';
300
+ if (op.key === "create_devbox") {
301
+ return status === "build_complete";
272
302
  }
273
303
  // Allow delete for any status
274
304
  return true;
@@ -276,92 +306,141 @@ const ListBlueprintsUI = () => {
276
306
  : allOperations;
277
307
  // Operation result display
278
308
  if (operationResult || operationError) {
279
- const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
309
+ const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
310
+ "Operation";
280
311
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
281
- { label: 'Blueprints' },
312
+ { label: "Blueprints" },
282
313
  {
283
- label: selectedBlueprint?.name || selectedBlueprint?.id || 'Blueprint',
314
+ label: selectedBlueprint?.name || selectedBlueprint?.id || "Blueprint",
284
315
  },
285
316
  { label: operationLabel, active: true },
286
- ] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && _jsx(ErrorMessage, { message: "Operation failed", error: operationError }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
317
+ ] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
287
318
  }
288
319
  // Operation input mode
289
320
  if (executingOperation && selectedBlueprint) {
290
321
  const currentOp = allOperations.find((op) => op.key === executingOperation);
291
322
  const needsInput = currentOp?.needsInput;
292
- const operationLabel = currentOp?.label || 'Operation';
323
+ const operationLabel = currentOp?.label || "Operation";
293
324
  if (loading) {
294
325
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
295
- { label: 'Blueprints' },
326
+ { label: "Blueprints" },
296
327
  { label: selectedBlueprint.name || selectedBlueprint.id },
297
328
  { label: operationLabel, active: true },
298
329
  ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
299
330
  }
300
331
  if (!needsInput) {
301
332
  const messages = {
302
- delete: 'Deleting blueprint...',
333
+ delete: "Deleting blueprint...",
303
334
  };
304
335
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
305
- { label: 'Blueprints' },
336
+ { label: "Blueprints" },
306
337
  { label: selectedBlueprint.name || selectedBlueprint.id },
307
338
  { label: operationLabel, active: true },
308
- ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
339
+ ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
309
340
  }
310
341
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
311
- { label: 'Blueprints' },
342
+ { label: "Blueprints" },
312
343
  { label: selectedBlueprint.name || selectedBlueprint.id },
313
344
  { label: operationLabel, active: true },
314
- ] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [currentOp.inputPrompt, " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp.inputPlaceholder || '' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
345
+ ] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp.inputPrompt, " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
315
346
  }
316
- // Details view with operation selection
317
- if (showDetails && selectedBlueprint) {
318
- const ds = selectedBlueprint.dockerfile_setup;
319
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
320
- { label: 'Blueprints' },
321
- {
322
- label: selectedBlueprint.name || selectedBlueprint.id,
323
- active: true,
324
- },
325
- ] }), _jsx(Header, { title: "Blueprint Details" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: selectedBlueprint.name || selectedBlueprint.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedBlueprint.status }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "\u2022 ", selectedBlueprint.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: selectedBlueprint.create_time_ms
326
- ? new Date(selectedBlueprint.create_time_ms).toLocaleString()
327
- : '' }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "(", selectedBlueprint.create_time_ms
328
- ? formatTimeAgo(selectedBlueprint.create_time_ms)
329
- : '', ")"] })] }), ds?.description && (_jsx(Box, { children: _jsx(Text, { color: "gray", dimColor: true, children: ds.description }) }))] }), ds && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "yellow", bold: true, children: [figures.squareSmallFilled, " Dockerfile Setup"] }), ds.base_image && (_jsxs(Text, { dimColor: true, children: ["Base Image: ", ds.base_image] })), ds.entrypoint && (_jsxs(Text, { dimColor: true, children: ["Entrypoint: ", ds.entrypoint] })), ds.system_packages && ds.system_packages.length > 0 && (_jsxs(Text, { dimColor: true, children: ["System Packages: ", ds.system_packages.join(', ')] })), ds.python_packages && ds.python_packages.length > 0 && (_jsxs(Text, { dimColor: true, children: ["Python Packages: ", ds.python_packages.join(', ')] }))] })), selectedBlueprint.metadata && Object.keys(selectedBlueprint.metadata).length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "green", paddingX: 1, paddingY: 0, children: _jsx(MetadataDisplay, { metadata: selectedBlueprint.metadata, showBorder: false }) })), selectedBlueprint.build_error && (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, paddingY: 0, children: [_jsxs(Text, { color: "red", bold: true, children: [figures.cross, ' '] }), _jsx(Text, { color: "red", dimColor: true, children: selectedBlueprint.build_error })] })), _jsx(OperationsMenu, { operations: operations, selectedIndex: selectedOperation, onNavigate: (direction) => {
330
- if (direction === 'up' && selectedOperation > 0) {
331
- setSelectedOperation(selectedOperation - 1);
332
- }
333
- else if (direction === 'down' && selectedOperation < operations.length - 1) {
334
- setSelectedOperation(selectedOperation + 1);
335
- }
336
- }, onSelect: (op) => {
337
- console.clear();
338
- setExecutingOperation(op.key);
339
- }, onBack: () => {
340
- console.clear();
341
- setShowDetails(false);
342
- setSelectedOperation(0);
343
- }, additionalActions: [{ key: 'o', label: 'Browser', handler: () => { } }] })] }));
347
+ // Create devbox screen
348
+ if (showCreateDevbox && selectedBlueprint) {
349
+ return (_jsx(DevboxCreatePage, { onBack: () => {
350
+ setShowCreateDevbox(false);
351
+ setSelectedBlueprint(null);
352
+ }, onCreate: (devbox) => {
353
+ // Return to blueprint list after creation
354
+ setShowCreateDevbox(false);
355
+ setSelectedBlueprint(null);
356
+ }, initialBlueprintId: selectedBlueprint.id }));
357
+ }
358
+ // Loading state
359
+ if (loading) {
360
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
344
361
  }
362
+ // Error state
363
+ if (listError) {
364
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
365
+ }
366
+ // Empty state
367
+ if (blueprints.length === 0) {
368
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsx(Text, { children: " No blueprints found. Try:" }), _jsx(Text, { color: colors.primary, bold: true, children: "rli blueprint create" })] })] }));
369
+ }
370
+ // Pagination moved earlier
371
+ // Overlay: draw quick actions popup over the table (keep table visible)
345
372
  // List view
346
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: 'Blueprints', active: true }] }), loading && _jsx(SpinnerComponent, { message: "Loading blueprints..." }), !loading && !error && blueprints.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No blueprints found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln blueprint create" })] })), !loading && !error && blueprints.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", buildComplete] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", building] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "red", children: [figures.cross, " ", failed] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", blueprints.length, blueprints.length >= MAX_FETCH && '+'] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Page ", currentPage + 1, "/", totalPages] })] }))] }), _jsx(Table, { data: currentBlueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, columns: [
347
- createComponentColumn('status', 'Status', (blueprint) => _jsx(StatusBadge, { status: blueprint.status, showText: false }), { width: 2 }),
348
- createTextColumn('id', 'ID', (blueprint) => (showFullId ? blueprint.id : blueprint.id.slice(0, 13)), {
349
- width: showFullId ? idWidth : 15,
350
- color: 'gray',
351
- dimColor: true,
352
- bold: false,
353
- }),
354
- createTextColumn('name', 'Name', (blueprint) => blueprint.name || '(unnamed)', { width: nameWidth }),
355
- createTextColumn('description', 'Description', (blueprint) => blueprint.dockerfile_setup?.description || '', {
356
- width: descriptionWidth,
357
- color: 'gray',
358
- dimColor: true,
359
- bold: false,
360
- visible: showDescription,
361
- }),
362
- createTextColumn('created', 'Created', (blueprint) => blueprint.create_time_ms ? formatTimeAgo(blueprint.create_time_ms) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
363
- ] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Operations \u2022 [o] Open in Browser \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[q] Quit"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list blueprints", error: error })] }));
373
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(Table, { data: currentBlueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${blueprints.length}]`, columns: [
374
+ {
375
+ key: "statusIcon",
376
+ label: "",
377
+ width: statusIconWidth,
378
+ render: (blueprint, index, isSelected) => {
379
+ const statusDisplay = getStatusDisplay(blueprint.status);
380
+ const statusColor = statusDisplay.color === colors.textDim
381
+ ? colors.info
382
+ : statusDisplay.color;
383
+ return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
384
+ },
385
+ },
386
+ {
387
+ key: "id",
388
+ label: "ID",
389
+ width: idWidth + 1,
390
+ render: (blueprint, index, isSelected) => {
391
+ const value = blueprint.id;
392
+ const width = idWidth + 1;
393
+ const truncated = value.slice(0, width - 1);
394
+ const padded = truncated.padEnd(width, " ");
395
+ return (_jsx(Text, { color: isSelected ? "white" : colors.textDim, bold: false, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
396
+ },
397
+ },
398
+ {
399
+ key: "statusText",
400
+ label: "Status",
401
+ width: statusTextWidth,
402
+ render: (blueprint, index, isSelected) => {
403
+ const statusDisplay = getStatusDisplay(blueprint.status);
404
+ const statusColor = statusDisplay.color === colors.textDim
405
+ ? colors.info
406
+ : statusDisplay.color;
407
+ const truncated = statusDisplay.text.slice(0, statusTextWidth);
408
+ const padded = truncated.padEnd(statusTextWidth, " ");
409
+ return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
410
+ },
411
+ },
412
+ createTextColumn("name", "Name", (blueprint) => blueprint.name || "(unnamed)", {
413
+ width: nameWidth,
414
+ }),
415
+ createTextColumn("description", "Description", (blueprint) => blueprint.dockerfile_setup?.description || "", {
416
+ width: descriptionWidth,
417
+ color: colors.textDim,
418
+ dimColor: false,
419
+ bold: false,
420
+ visible: showDescription,
421
+ }),
422
+ createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
423
+ ? formatTimeAgo(blueprint.create_time_ms)
424
+ : "", {
425
+ width: timeWidth,
426
+ color: colors.textDim,
427
+ dimColor: false,
428
+ bold: false,
429
+ }),
430
+ ] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", blueprints.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] })] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", blueprints.length] })] }), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: -Math.min(allOperations.length + 10, PAGE_SIZE + 5), justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
431
+ key: op.key,
432
+ label: op.label,
433
+ color: op.color,
434
+ icon: op.icon,
435
+ shortcut: op.key === "create_devbox"
436
+ ? "c"
437
+ : op.key === "delete"
438
+ ? "d"
439
+ : "",
440
+ })), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), totalPages > 1 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [o] Browser"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
364
441
  };
442
+ // Export the UI component for use in the main menu
443
+ export { ListBlueprintsUI };
365
444
  export async function listBlueprints(options = {}) {
366
445
  const executor = createExecutor(options);
367
446
  await executor.executeList(async () => {