@runloop/rl-cli 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +64 -29
  2. package/dist/cli.js +401 -92
  3. package/dist/commands/auth.js +12 -11
  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 +293 -225
  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 +328 -190
  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 +20 -20
  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 +56 -97
  40. package/dist/commands/snapshot/status.js +37 -0
  41. package/dist/components/ActionsPopup.js +16 -13
  42. package/dist/components/Banner.js +4 -4
  43. package/dist/components/Breadcrumb.js +55 -5
  44. package/dist/components/DetailView.js +7 -4
  45. package/dist/components/DevboxActionsMenu.js +315 -178
  46. package/dist/components/DevboxCard.js +15 -14
  47. package/dist/components/DevboxCreatePage.js +147 -113
  48. package/dist/components/DevboxDetailPage.js +180 -102
  49. package/dist/components/ErrorMessage.js +5 -4
  50. package/dist/components/Header.js +4 -3
  51. package/dist/components/MainMenu.js +34 -33
  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 +16 -12
  65. package/dist/utils/client.js +7 -7
  66. package/dist/utils/config.js +130 -4
  67. package/dist/utils/interactiveCommand.js +2 -2
  68. package/dist/utils/output.js +17 -17
  69. package/dist/utils/ssh.js +160 -0
  70. package/dist/utils/sshSession.js +16 -12
  71. package/dist/utils/theme.js +22 -0
  72. package/dist/utils/url.js +4 -4
  73. package/package.json +29 -4
@@ -1,151 +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';
17
- import { getBlueprintUrl } from '../../utils/url.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";
18
20
  const PAGE_SIZE = 10;
19
21
  const MAX_FETCH = 100;
20
- // Format time ago
21
- const formatTimeAgo = (timestamp) => {
22
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
23
- if (seconds < 60)
24
- return `${seconds}s ago`;
25
- const minutes = Math.floor(seconds / 60);
26
- if (minutes < 60)
27
- return `${minutes}m ago`;
28
- const hours = Math.floor(minutes / 60);
29
- if (hours < 24)
30
- return `${hours}h ago`;
31
- const days = Math.floor(hours / 24);
32
- if (days < 30)
33
- return `${days}d ago`;
34
- const months = Math.floor(days / 30);
35
- if (months < 12)
36
- return `${months}mo ago`;
37
- const years = Math.floor(months / 12);
38
- return `${years}y ago`;
39
- };
40
22
  const ListBlueprintsUI = ({ onBack, onExit }) => {
41
23
  const { stdout } = useStdout();
42
- const { exit: inkExit } = useApp();
43
- const [loading, setLoading] = React.useState(true);
44
- const [blueprints, setBlueprints] = React.useState([]);
45
- const [error, setError] = React.useState(null);
46
- const [currentPage, setCurrentPage] = React.useState(0);
47
- const [selectedIndex, setSelectedIndex] = React.useState(0);
48
- const [showDetails, setShowDetails] = React.useState(false);
24
+ const [selectedBlueprint, setSelectedBlueprint] = React.useState(null);
49
25
  const [selectedOperation, setSelectedOperation] = React.useState(0);
50
26
  const [executingOperation, setExecutingOperation] = React.useState(null);
51
- const [operationInput, setOperationInput] = React.useState('');
27
+ const [operationInput, setOperationInput] = React.useState("");
52
28
  const [operationResult, setOperationResult] = React.useState(null);
53
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);
54
39
  // Calculate responsive column widths
55
40
  const terminalWidth = stdout?.columns || 120;
56
41
  const showDescription = terminalWidth >= 120;
57
- const showFullId = terminalWidth >= 80;
42
+ const statusIconWidth = 2;
43
+ const statusTextWidth = 10;
58
44
  const idWidth = 25;
59
45
  const nameWidth = terminalWidth >= 120 ? 30 : 25;
60
46
  const descriptionWidth = 40;
61
47
  const timeWidth = 20;
62
- const allOperations = [
63
- {
64
- key: 'create_devbox',
65
- label: 'Create Devbox from Blueprint',
66
- color: 'green',
67
- icon: figures.play,
68
- needsInput: true,
69
- inputPrompt: 'Devbox name (optional):',
70
- inputPlaceholder: 'my-devbox',
71
- },
72
- {
73
- key: 'delete',
74
- label: 'Delete Blueprint',
75
- 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,
76
67
  icon: figures.cross,
77
- },
78
- ];
68
+ });
69
+ return operations;
70
+ };
71
+ // Fetch blueprints - moved to top to ensure hooks are called in same order
79
72
  React.useEffect(() => {
80
- const list = async () => {
73
+ const fetchBlueprints = async () => {
81
74
  try {
75
+ setLoading(true);
82
76
  const client = getClient();
83
77
  const allBlueprints = [];
84
78
  let count = 0;
85
79
  for await (const blueprint of client.blueprints.list()) {
86
80
  allBlueprints.push(blueprint);
87
81
  count++;
88
- if (count >= MAX_FETCH) {
82
+ if (count >= MAX_FETCH)
89
83
  break;
90
- }
91
84
  }
92
85
  setBlueprints(allBlueprints);
93
86
  }
94
87
  catch (err) {
95
- setError(err);
88
+ setListError(err);
96
89
  }
97
90
  finally {
98
91
  setLoading(false);
99
92
  }
100
93
  };
101
- list();
94
+ fetchBlueprints();
102
95
  }, []);
103
- // Clear console when transitioning to detail view
104
- const prevShowDetailsRef = React.useRef(showDetails);
105
- React.useEffect(() => {
106
- if (showDetails && !prevShowDetailsRef.current) {
107
- console.clear();
108
- }
109
- prevShowDetailsRef.current = showDetails;
110
- }, [showDetails]);
111
- // Auto-execute operations that don't need input
112
- React.useEffect(() => {
113
- if (executingOperation === 'delete' && !loading && selectedBlueprint) {
114
- executeOperation();
115
- }
116
- }, [executingOperation]);
117
- const executeOperation = async () => {
118
- const client = getClient();
119
- const blueprint = selectedBlueprint;
120
- try {
121
- setLoading(true);
122
- switch (executingOperation) {
123
- case 'create_devbox':
124
- const devbox = await client.devboxes.create({
125
- blueprint_id: blueprint.id,
126
- name: operationInput || undefined,
127
- });
128
- setOperationResult(`Devbox created successfully!\n\n` +
129
- `ID: ${devbox.id}\n` +
130
- `Name: ${devbox.name || '(none)'}\n` +
131
- `Status: ${devbox.status}\n\n` +
132
- `Use 'rln devbox list' to view all devboxes.`);
133
- break;
134
- case 'delete':
135
- await client.blueprints.delete(blueprint.id);
136
- setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
137
- break;
138
- }
139
- }
140
- catch (err) {
141
- setOperationError(err);
142
- }
143
- finally {
144
- setLoading(false);
145
- }
146
- };
96
+ // Handle input for all views - combined into single hook
147
97
  useInput((input, key) => {
148
- 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
+ }
149
103
  // Handle operation input mode
150
104
  if (executingOperation && !operationResult && !operationError) {
151
105
  const currentOp = allOperations.find((op) => op.key === executingOperation);
@@ -153,95 +107,130 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
153
107
  if (key.return) {
154
108
  executeOperation();
155
109
  }
156
- else if (input === 'q' || key.escape) {
110
+ else if (input === "q" || key.escape) {
157
111
  console.clear();
158
112
  setExecutingOperation(null);
159
- setOperationInput('');
113
+ setOperationInput("");
160
114
  }
161
115
  }
162
116
  return;
163
117
  }
164
118
  // Handle operation result display
165
119
  if (operationResult || operationError) {
166
- if (input === 'q' || key.escape || key.return) {
120
+ if (input === "q" || key.escape || key.return) {
167
121
  console.clear();
168
122
  setOperationResult(null);
169
123
  setOperationError(null);
170
124
  setExecutingOperation(null);
171
- setOperationInput('');
125
+ setOperationInput("");
172
126
  }
173
127
  return;
174
128
  }
175
- // Handle details view
176
- if (showDetails) {
177
- if (input === 'q' || key.escape) {
178
- console.clear();
179
- setShowDetails(false);
180
- setSelectedOperation(0);
181
- }
182
- else if (input === 'o' && selectedBlueprint) {
183
- // Open in browser
184
- const url = getBlueprintUrl(selectedBlueprint.id);
185
- const openBrowser = async () => {
186
- const { exec } = await import('child_process');
187
- const platform = process.platform;
188
- let openCommand;
189
- if (platform === 'darwin') {
190
- openCommand = `open "${url}"`;
191
- }
192
- else if (platform === 'win32') {
193
- openCommand = `start "${url}"`;
194
- }
195
- else {
196
- openCommand = `xdg-open "${url}"`;
197
- }
198
- exec(openCommand);
199
- };
200
- openBrowser();
201
- }
202
- 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) {
203
136
  setSelectedOperation(selectedOperation - 1);
204
137
  }
205
- else if (key.downArrow && selectedOperation < operations.length - 1) {
138
+ else if (key.downArrow &&
139
+ selectedOperation < allOperations.length - 1) {
206
140
  setSelectedOperation(selectedOperation + 1);
207
141
  }
208
142
  else if (key.return) {
209
143
  console.clear();
210
- const op = operations[selectedOperation].key;
211
- 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);
212
193
  }
213
194
  return;
214
195
  }
215
- // 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;
216
203
  if (key.upArrow && selectedIndex > 0) {
217
204
  setSelectedIndex(selectedIndex - 1);
218
205
  }
219
206
  else if (key.downArrow && selectedIndex < pageBlueprints - 1) {
220
207
  setSelectedIndex(selectedIndex + 1);
221
208
  }
222
- else if ((input === 'n' || key.rightArrow) && currentPage < totalPages - 1) {
209
+ else if ((input === "n" || key.rightArrow) &&
210
+ currentPage < totalPages - 1) {
223
211
  setCurrentPage(currentPage + 1);
224
212
  setSelectedIndex(0);
225
213
  }
226
- else if ((input === 'p' || key.leftArrow) && currentPage > 0) {
214
+ else if ((input === "p" || key.leftArrow) && currentPage > 0) {
227
215
  setCurrentPage(currentPage - 1);
228
216
  setSelectedIndex(0);
229
217
  }
230
- else if (key.return) {
218
+ else if (input === "a") {
231
219
  console.clear();
232
- setShowDetails(true);
220
+ setShowPopup(true);
221
+ setSelectedOperation(0);
233
222
  }
234
- else if (input === 'o' && selectedBlueprint) {
223
+ else if (input === "o" && currentBlueprints[selectedIndex]) {
235
224
  // Open in browser
236
- const url = getBlueprintUrl(selectedBlueprint.id);
225
+ const url = getBlueprintUrl(currentBlueprints[selectedIndex].id);
237
226
  const openBrowser = async () => {
238
- const { exec } = await import('child_process');
227
+ const { exec } = await import("child_process");
239
228
  const platform = process.platform;
240
229
  let openCommand;
241
- if (platform === 'darwin') {
230
+ if (platform === "darwin") {
242
231
  openCommand = `open "${url}"`;
243
232
  }
244
- else if (platform === 'win32') {
233
+ else if (platform === "win32") {
245
234
  openCommand = `start "${url}"`;
246
235
  }
247
236
  else {
@@ -258,26 +247,58 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
258
247
  else if (onExit) {
259
248
  onExit();
260
249
  }
261
- else {
262
- inkExit();
263
- }
264
250
  }
265
251
  });
266
- const totalPages = Math.ceil(blueprints.length / PAGE_SIZE);
267
- const startIndex = currentPage * PAGE_SIZE;
268
- 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);
269
257
  const currentBlueprints = blueprints.slice(startIndex, endIndex);
270
- const selectedBlueprint = currentBlueprints[selectedIndex];
271
- const buildComplete = blueprints.filter((b) => b.status === 'build_complete').length;
272
- const building = blueprints.filter((b) => ['provisioning', 'building'].includes(b.status)).length;
273
- 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
+ };
274
295
  // Filter operations based on blueprint status
275
296
  const operations = selectedBlueprint
276
297
  ? allOperations.filter((op) => {
277
298
  const status = selectedBlueprint.status;
278
299
  // Only allow creating devbox if build is complete
279
- if (op.key === 'create_devbox') {
280
- return status === 'build_complete';
300
+ if (op.key === "create_devbox") {
301
+ return status === "build_complete";
281
302
  }
282
303
  // Allow delete for any status
283
304
  return true;
@@ -285,91 +306,138 @@ const ListBlueprintsUI = ({ onBack, onExit }) => {
285
306
  : allOperations;
286
307
  // Operation result display
287
308
  if (operationResult || operationError) {
288
- const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
309
+ const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
310
+ "Operation";
289
311
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
290
- { label: 'Blueprints' },
312
+ { label: "Blueprints" },
291
313
  {
292
- label: selectedBlueprint?.name || selectedBlueprint?.id || 'Blueprint',
314
+ label: selectedBlueprint?.name || selectedBlueprint?.id || "Blueprint",
293
315
  },
294
316
  { label: operationLabel, active: true },
295
- ] }), _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" }) })] }));
296
318
  }
297
319
  // Operation input mode
298
320
  if (executingOperation && selectedBlueprint) {
299
321
  const currentOp = allOperations.find((op) => op.key === executingOperation);
300
322
  const needsInput = currentOp?.needsInput;
301
- const operationLabel = currentOp?.label || 'Operation';
323
+ const operationLabel = currentOp?.label || "Operation";
302
324
  if (loading) {
303
325
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
304
- { label: 'Blueprints' },
326
+ { label: "Blueprints" },
305
327
  { label: selectedBlueprint.name || selectedBlueprint.id },
306
328
  { label: operationLabel, active: true },
307
329
  ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
308
330
  }
309
331
  if (!needsInput) {
310
332
  const messages = {
311
- delete: 'Deleting blueprint...',
333
+ delete: "Deleting blueprint...",
312
334
  };
313
335
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
314
- { label: 'Blueprints' },
336
+ { label: "Blueprints" },
315
337
  { label: selectedBlueprint.name || selectedBlueprint.id },
316
338
  { label: operationLabel, active: true },
317
- ] }), _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..." })] }));
318
340
  }
319
341
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
320
- { label: 'Blueprints' },
342
+ { label: "Blueprints" },
321
343
  { label: selectedBlueprint.name || selectedBlueprint.id },
322
344
  { label: operationLabel, active: true },
323
- ] }), _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" }) })] })] }));
324
346
  }
325
- // Details view with operation selection
326
- if (showDetails && selectedBlueprint) {
327
- const ds = selectedBlueprint.dockerfile_setup;
328
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
329
- { label: 'Blueprints' },
330
- {
331
- label: selectedBlueprint.name || selectedBlueprint.id,
332
- active: true,
333
- },
334
- ] }), _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
335
- ? new Date(selectedBlueprint.create_time_ms).toLocaleString()
336
- : '' }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "(", selectedBlueprint.create_time_ms
337
- ? formatTimeAgo(selectedBlueprint.create_time_ms)
338
- : '', ")"] })] }), 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) => {
339
- if (direction === 'up' && selectedOperation > 0) {
340
- setSelectedOperation(selectedOperation - 1);
341
- }
342
- else if (direction === 'down' && selectedOperation < operations.length - 1) {
343
- setSelectedOperation(selectedOperation + 1);
344
- }
345
- }, onSelect: (op) => {
346
- console.clear();
347
- setExecutingOperation(op.key);
348
- }, onBack: () => {
349
- console.clear();
350
- setShowDetails(false);
351
- setSelectedOperation(0);
352
- }, 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 }], showVersionCheck: true }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
353
361
  }
362
+ // Error state
363
+ if (listError) {
364
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }], showVersionCheck: 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 }], showVersionCheck: 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)
354
372
  // List view
355
- 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: [
356
- createComponentColumn('status', 'Status', (blueprint) => _jsx(StatusBadge, { status: blueprint.status, showText: false }), { width: 2 }),
357
- createTextColumn('id', 'ID', (blueprint) => (showFullId ? blueprint.id : blueprint.id.slice(0, 13)), {
358
- width: showFullId ? idWidth : 15,
359
- color: 'gray',
360
- dimColor: true,
361
- bold: false,
362
- }),
363
- createTextColumn('name', 'Name', (blueprint) => blueprint.name || '(unnamed)', { width: nameWidth }),
364
- createTextColumn('description', 'Description', (blueprint) => blueprint.dockerfile_setup?.description || '', {
365
- width: descriptionWidth,
366
- color: 'gray',
367
- dimColor: true,
368
- bold: false,
369
- visible: showDescription,
370
- }),
371
- createTextColumn('created', 'Created', (blueprint) => blueprint.create_time_ms ? formatTimeAgo(blueprint.create_time_ms) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
372
- ] }), _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: [' ', "[Esc] Back"] })] })] })), 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"] })] })] }));
373
441
  };
374
442
  // Export the UI component for use in the main menu
375
443
  export { ListBlueprintsUI };