@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,62 +1,125 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { Box, Text, useInput, useApp, 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 './Header.js';
8
- import { SpinnerComponent } from './Spinner.js';
9
- import { ErrorMessage } from './ErrorMessage.js';
10
- import { SuccessMessage } from './SuccessMessage.js';
11
- import { Breadcrumb } from './Breadcrumb.js';
2
+ import React from "react";
3
+ import { Box, Text, useInput, useApp, 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 "./Header.js";
8
+ import { SpinnerComponent } from "./Spinner.js";
9
+ import { ErrorMessage } from "./ErrorMessage.js";
10
+ import { SuccessMessage } from "./SuccessMessage.js";
11
+ import { Breadcrumb } from "./Breadcrumb.js";
12
+ import { colors } from "../utils/theme.js";
12
13
  export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
13
- { label: 'Devboxes' },
14
- { label: devbox.name || devbox.id, active: true }
15
- ], initialOperation, initialOperationIndex = 0, }) => {
14
+ { label: "Devboxes" },
15
+ { label: devbox.name || devbox.id, active: true },
16
+ ], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, onSSHRequest, }) => {
16
17
  const { exit } = useApp();
17
18
  const { stdout } = useStdout();
18
19
  const [loading, setLoading] = React.useState(false);
19
20
  const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
20
21
  const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
21
- const [operationInput, setOperationInput] = React.useState('');
22
+ const [operationInput, setOperationInput] = React.useState("");
22
23
  const [operationResult, setOperationResult] = React.useState(null);
23
24
  const [operationError, setOperationError] = React.useState(null);
24
- const [logsWrapMode, setLogsWrapMode] = React.useState(true);
25
+ const [logsWrapMode, setLogsWrapMode] = React.useState(false);
25
26
  const [logsScroll, setLogsScroll] = React.useState(0);
26
27
  const [execScroll, setExecScroll] = React.useState(0);
27
28
  const [copyStatus, setCopyStatus] = React.useState(null);
28
29
  const allOperations = [
29
- { key: 'logs', label: 'View Logs', color: 'blue', icon: figures.info, shortcut: 'l' },
30
- { key: 'exec', label: 'Execute Command', color: 'green', icon: figures.play, shortcut: 'e' },
31
- { key: 'upload', label: 'Upload File', color: 'green', icon: figures.arrowUp, shortcut: 'u' },
32
- { key: 'snapshot', label: 'Create Snapshot', color: 'yellow', icon: figures.circleFilled, shortcut: 'n' },
33
- { key: 'ssh', label: 'SSH onto the box', color: 'cyan', icon: figures.arrowRight, shortcut: 's' },
34
- { key: 'tunnel', label: 'Open Tunnel', color: 'magenta', icon: figures.pointerSmall, shortcut: 't' },
35
- { key: 'suspend', label: 'Suspend Devbox', color: 'yellow', icon: figures.squareSmallFilled, shortcut: 'p' },
36
- { key: 'resume', label: 'Resume Devbox', color: 'green', icon: figures.play, shortcut: 'r' },
37
- { key: 'delete', label: 'Shutdown Devbox', color: 'red', icon: figures.cross, shortcut: 'd' },
30
+ {
31
+ key: "logs",
32
+ label: "View Logs",
33
+ color: colors.info,
34
+ icon: figures.info,
35
+ shortcut: "l",
36
+ },
37
+ {
38
+ key: "exec",
39
+ label: "Execute Command",
40
+ color: colors.success,
41
+ icon: figures.play,
42
+ shortcut: "e",
43
+ },
44
+ {
45
+ key: "upload",
46
+ label: "Upload File",
47
+ color: colors.success,
48
+ icon: figures.arrowUp,
49
+ shortcut: "u",
50
+ },
51
+ {
52
+ key: "snapshot",
53
+ label: "Create Snapshot",
54
+ color: colors.warning,
55
+ icon: figures.circleFilled,
56
+ shortcut: "n",
57
+ },
58
+ {
59
+ key: "ssh",
60
+ label: "SSH onto the box",
61
+ color: colors.primary,
62
+ icon: figures.arrowRight,
63
+ shortcut: "s",
64
+ },
65
+ {
66
+ key: "tunnel",
67
+ label: "Open Tunnel",
68
+ color: colors.secondary,
69
+ icon: figures.pointerSmall,
70
+ shortcut: "t",
71
+ },
72
+ {
73
+ key: "suspend",
74
+ label: "Suspend Devbox",
75
+ color: colors.warning,
76
+ icon: figures.squareSmallFilled,
77
+ shortcut: "p",
78
+ },
79
+ {
80
+ key: "resume",
81
+ label: "Resume Devbox",
82
+ color: colors.success,
83
+ icon: figures.play,
84
+ shortcut: "r",
85
+ },
86
+ {
87
+ key: "delete",
88
+ label: "Shutdown Devbox",
89
+ color: colors.error,
90
+ icon: figures.cross,
91
+ shortcut: "d",
92
+ },
38
93
  ];
39
94
  // Filter operations based on devbox status
40
- const operations = devbox ? allOperations.filter(op => {
41
- const status = devbox.status;
42
- // When suspended: logs and resume
43
- if (status === 'suspended') {
44
- return op.key === 'resume' || op.key === 'logs';
45
- }
46
- // When not running (shutdown, failure, etc): only logs
47
- if (status !== 'running' && status !== 'provisioning' && status !== 'initializing') {
48
- return op.key === 'logs';
49
- }
50
- // When running: everything except resume
51
- if (status === 'running') {
52
- return op.key !== 'resume';
53
- }
54
- // Default for transitional states (provisioning, initializing)
55
- return op.key === 'logs' || op.key === 'delete';
56
- }) : allOperations;
95
+ const operations = devbox
96
+ ? allOperations.filter((op) => {
97
+ const status = devbox.status;
98
+ // When suspended: logs and resume
99
+ if (status === "suspended") {
100
+ return op.key === "resume" || op.key === "logs";
101
+ }
102
+ // When not running (shutdown, failure, etc): only logs
103
+ if (status !== "running" &&
104
+ status !== "provisioning" &&
105
+ status !== "initializing") {
106
+ return op.key === "logs";
107
+ }
108
+ // When running: everything except resume
109
+ if (status === "running") {
110
+ return op.key !== "resume";
111
+ }
112
+ // Default for transitional states (provisioning, initializing)
113
+ return op.key === "logs" || op.key === "delete";
114
+ })
115
+ : allOperations;
57
116
  // Auto-execute operations that don't need input
58
117
  React.useEffect(() => {
59
- if ((executingOperation === 'delete' || executingOperation === 'ssh' || executingOperation === 'logs' || executingOperation === 'suspend' || executingOperation === 'resume') && !loading && devbox) {
118
+ const autoExecuteOps = ["delete", "ssh", "logs", "suspend", "resume"];
119
+ if (executingOperation &&
120
+ autoExecuteOps.includes(executingOperation) &&
121
+ !loading &&
122
+ devbox) {
60
123
  executeOperation();
61
124
  }
62
125
  }, [executingOperation]);
@@ -66,157 +129,216 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
66
129
  if (key.return && operationInput.trim()) {
67
130
  executeOperation();
68
131
  }
69
- else if (input === 'q' || key.escape) {
132
+ else if (input === "q" || key.escape) {
70
133
  console.clear();
71
134
  setExecutingOperation(null);
72
- setOperationInput('');
135
+ setOperationInput("");
73
136
  }
74
137
  return;
75
138
  }
76
139
  // Handle operation result display
77
140
  if (operationResult || operationError) {
78
- if (input === 'q' || key.escape || key.return) {
141
+ if (input === "q" || key.escape || key.return) {
79
142
  console.clear();
80
- setOperationResult(null);
81
- setOperationError(null);
82
- setExecutingOperation(null);
83
- setOperationInput('');
84
- setLogsWrapMode(true);
85
- setLogsScroll(0);
86
- setExecScroll(0);
87
- setCopyStatus(null);
143
+ // If skipOperationsMenu is true, go back to parent instead of operations menu
144
+ if (skipOperationsMenu) {
145
+ onBack();
146
+ }
147
+ else {
148
+ setOperationResult(null);
149
+ setOperationError(null);
150
+ setExecutingOperation(null);
151
+ setOperationInput("");
152
+ setLogsWrapMode(true);
153
+ setLogsScroll(0);
154
+ setExecScroll(0);
155
+ setCopyStatus(null);
156
+ }
88
157
  }
89
- else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
158
+ else if ((key.upArrow || input === "k") &&
159
+ operationResult &&
160
+ typeof operationResult === "object" &&
161
+ operationResult.__customRender === "exec") {
90
162
  setExecScroll(Math.max(0, execScroll - 1));
91
163
  }
92
- else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
164
+ else if ((key.downArrow || input === "j") &&
165
+ operationResult &&
166
+ typeof operationResult === "object" &&
167
+ operationResult.__customRender === "exec") {
93
168
  setExecScroll(execScroll + 1);
94
169
  }
95
- else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
170
+ else if (key.pageUp &&
171
+ operationResult &&
172
+ typeof operationResult === "object" &&
173
+ operationResult.__customRender === "exec") {
96
174
  setExecScroll(Math.max(0, execScroll - 10));
97
175
  }
98
- else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
176
+ else if (key.pageDown &&
177
+ operationResult &&
178
+ typeof operationResult === "object" &&
179
+ operationResult.__customRender === "exec") {
99
180
  setExecScroll(execScroll + 10);
100
181
  }
101
- else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
182
+ else if (input === "g" &&
183
+ operationResult &&
184
+ typeof operationResult === "object" &&
185
+ operationResult.__customRender === "exec") {
102
186
  setExecScroll(0);
103
187
  }
104
- else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
105
- const lines = [...(operationResult.stdout || '').split('\n'), ...(operationResult.stderr || '').split('\n')];
188
+ else if (input === "G" &&
189
+ operationResult &&
190
+ typeof operationResult === "object" &&
191
+ operationResult.__customRender === "exec") {
192
+ const lines = [
193
+ ...(operationResult.stdout || "").split("\n"),
194
+ ...(operationResult.stderr || "").split("\n"),
195
+ ];
106
196
  const terminalHeight = stdout?.rows || 30;
107
197
  const viewportHeight = Math.max(10, terminalHeight - 15);
108
198
  const maxScroll = Math.max(0, lines.length - viewportHeight);
109
199
  setExecScroll(maxScroll);
110
200
  }
111
- else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
201
+ else if (input === "c" &&
202
+ operationResult &&
203
+ typeof operationResult === "object" &&
204
+ operationResult.__customRender === "exec") {
112
205
  // Copy exec output to clipboard
113
- const output = (operationResult.stdout || '') + (operationResult.stderr || '');
206
+ const output = (operationResult.stdout || "") +
207
+ (operationResult.stderr || "");
114
208
  const copyToClipboard = async (text) => {
115
- const { spawn } = await import('child_process');
209
+ const { spawn } = await import("child_process");
116
210
  const platform = process.platform;
117
211
  let command;
118
212
  let args;
119
- if (platform === 'darwin') {
120
- command = 'pbcopy';
213
+ if (platform === "darwin") {
214
+ command = "pbcopy";
121
215
  args = [];
122
216
  }
123
- else if (platform === 'win32') {
124
- command = 'clip';
217
+ else if (platform === "win32") {
218
+ command = "clip";
125
219
  args = [];
126
220
  }
127
221
  else {
128
- command = 'xclip';
129
- args = ['-selection', 'clipboard'];
222
+ command = "xclip";
223
+ args = ["-selection", "clipboard"];
130
224
  }
131
225
  const proc = spawn(command, args);
132
226
  proc.stdin.write(text);
133
227
  proc.stdin.end();
134
- proc.on('exit', (code) => {
228
+ proc.on("exit", (code) => {
135
229
  if (code === 0) {
136
- setCopyStatus('Copied to clipboard!');
230
+ setCopyStatus("Copied to clipboard!");
137
231
  setTimeout(() => setCopyStatus(null), 2000);
138
232
  }
139
233
  else {
140
- setCopyStatus('Failed to copy');
234
+ setCopyStatus("Failed to copy");
141
235
  setTimeout(() => setCopyStatus(null), 2000);
142
236
  }
143
237
  });
144
- proc.on('error', () => {
145
- setCopyStatus('Copy not supported');
238
+ proc.on("error", () => {
239
+ setCopyStatus("Copy not supported");
146
240
  setTimeout(() => setCopyStatus(null), 2000);
147
241
  });
148
242
  };
149
243
  copyToClipboard(output);
150
244
  }
151
- else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
245
+ else if ((key.upArrow || input === "k") &&
246
+ operationResult &&
247
+ typeof operationResult === "object" &&
248
+ operationResult.__customRender === "logs") {
152
249
  setLogsScroll(Math.max(0, logsScroll - 1));
153
250
  }
154
- else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
251
+ else if ((key.downArrow || input === "j") &&
252
+ operationResult &&
253
+ typeof operationResult === "object" &&
254
+ operationResult.__customRender === "logs") {
155
255
  setLogsScroll(logsScroll + 1);
156
256
  }
157
- else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
257
+ else if (key.pageUp &&
258
+ operationResult &&
259
+ typeof operationResult === "object" &&
260
+ operationResult.__customRender === "logs") {
158
261
  setLogsScroll(Math.max(0, logsScroll - 10));
159
262
  }
160
- else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
263
+ else if (key.pageDown &&
264
+ operationResult &&
265
+ typeof operationResult === "object" &&
266
+ operationResult.__customRender === "logs") {
161
267
  setLogsScroll(logsScroll + 10);
162
268
  }
163
- else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
269
+ else if (input === "g" &&
270
+ operationResult &&
271
+ typeof operationResult === "object" &&
272
+ operationResult.__customRender === "logs") {
164
273
  setLogsScroll(0);
165
274
  }
166
- else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
275
+ else if (input === "G" &&
276
+ operationResult &&
277
+ typeof operationResult === "object" &&
278
+ operationResult.__customRender === "logs") {
167
279
  const logs = operationResult.__logs || [];
168
280
  const terminalHeight = stdout?.rows || 30;
169
281
  const viewportHeight = Math.max(10, terminalHeight - 10);
170
282
  const maxScroll = Math.max(0, logs.length - viewportHeight);
171
283
  setLogsScroll(maxScroll);
172
284
  }
173
- else if (input === 'w' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
285
+ else if (input === "w" &&
286
+ operationResult &&
287
+ typeof operationResult === "object" &&
288
+ operationResult.__customRender === "logs") {
174
289
  setLogsWrapMode(!logsWrapMode);
175
290
  }
176
- else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
291
+ else if (input === "c" &&
292
+ operationResult &&
293
+ typeof operationResult === "object" &&
294
+ operationResult.__customRender === "logs") {
177
295
  // Copy logs to clipboard
178
296
  const logs = operationResult.__logs || [];
179
- const logsText = logs.map((log) => {
297
+ const logsText = logs
298
+ .map((log) => {
180
299
  const time = new Date(log.timestamp_ms).toLocaleString();
181
- const level = log.level || 'INFO';
182
- const source = log.source || 'exec';
183
- const message = log.message || '';
184
- const cmd = log.cmd ? `[${log.cmd}] ` : '';
185
- const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
300
+ const level = log.level || "INFO";
301
+ const source = log.source || "exec";
302
+ const message = log.message || "";
303
+ const cmd = log.cmd ? `[${log.cmd}] ` : "";
304
+ const exitCode = log.exit_code !== null && log.exit_code !== undefined
305
+ ? `(${log.exit_code}) `
306
+ : "";
186
307
  return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
187
- }).join('\n');
308
+ })
309
+ .join("\n");
188
310
  const copyToClipboard = async (text) => {
189
- const { spawn } = await import('child_process');
311
+ const { spawn } = await import("child_process");
190
312
  const platform = process.platform;
191
313
  let command;
192
314
  let args;
193
- if (platform === 'darwin') {
194
- command = 'pbcopy';
315
+ if (platform === "darwin") {
316
+ command = "pbcopy";
195
317
  args = [];
196
318
  }
197
- else if (platform === 'win32') {
198
- command = 'clip';
319
+ else if (platform === "win32") {
320
+ command = "clip";
199
321
  args = [];
200
322
  }
201
323
  else {
202
- command = 'xclip';
203
- args = ['-selection', 'clipboard'];
324
+ command = "xclip";
325
+ args = ["-selection", "clipboard"];
204
326
  }
205
327
  const proc = spawn(command, args);
206
328
  proc.stdin.write(text);
207
329
  proc.stdin.end();
208
- proc.on('exit', (code) => {
330
+ proc.on("exit", (code) => {
209
331
  if (code === 0) {
210
- setCopyStatus('Copied to clipboard!');
332
+ setCopyStatus("Copied to clipboard!");
211
333
  setTimeout(() => setCopyStatus(null), 2000);
212
334
  }
213
335
  else {
214
- setCopyStatus('Failed to copy');
336
+ setCopyStatus("Failed to copy");
215
337
  setTimeout(() => setCopyStatus(null), 2000);
216
338
  }
217
339
  });
218
- proc.on('error', () => {
219
- setCopyStatus('Copy not supported');
340
+ proc.on("error", () => {
341
+ setCopyStatus("Copy not supported");
220
342
  setTimeout(() => setCopyStatus(null), 2000);
221
343
  });
222
344
  };
@@ -225,7 +347,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
225
347
  return;
226
348
  }
227
349
  // Operations selection mode
228
- if (input === 'q' || key.escape) {
350
+ if (input === "q" || key.escape) {
229
351
  console.clear();
230
352
  onBack();
231
353
  setSelectedOperation(0);
@@ -243,7 +365,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
243
365
  }
244
366
  else if (input) {
245
367
  // Check if input matches any operation shortcut
246
- const matchedOp = operations.find(op => op.shortcut === input);
368
+ const matchedOp = operations.find((op) => op.shortcut === input);
247
369
  if (matchedOp) {
248
370
  console.clear();
249
371
  setExecutingOperation(matchedOp.key);
@@ -255,90 +377,104 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
255
377
  try {
256
378
  setLoading(true);
257
379
  switch (executingOperation) {
258
- case 'exec':
380
+ case "exec":
259
381
  const execResult = await client.devboxes.executeSync(devbox.id, {
260
382
  command: operationInput,
261
383
  });
262
384
  // Format exec result for custom rendering
263
385
  const formattedExecResult = {
264
- __customRender: 'exec',
386
+ __customRender: "exec",
265
387
  command: operationInput,
266
- stdout: execResult.stdout || '',
267
- stderr: execResult.stderr || '',
388
+ stdout: execResult.stdout || "",
389
+ stderr: execResult.stderr || "",
268
390
  exitCode: execResult.exit_code ?? 0,
269
391
  };
270
392
  setOperationResult(formattedExecResult);
271
393
  break;
272
- case 'upload':
273
- const fs = await import('fs');
394
+ case "upload":
395
+ const fs = await import("fs");
274
396
  const fileStream = fs.createReadStream(operationInput);
275
- const filename = operationInput.split('/').pop() || 'file';
397
+ const filename = operationInput.split("/").pop() || "file";
276
398
  await client.devboxes.uploadFile(devbox.id, {
277
399
  path: filename,
278
400
  file: fileStream,
279
401
  });
280
402
  setOperationResult(`File ${filename} uploaded successfully`);
281
403
  break;
282
- case 'snapshot':
404
+ case "snapshot":
283
405
  const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
284
406
  name: operationInput || `snapshot-${Date.now()}`,
285
407
  });
286
408
  setOperationResult(`Snapshot created: ${snapshot.id}`);
287
409
  break;
288
- case 'ssh':
410
+ case "ssh":
289
411
  const sshKey = await client.devboxes.createSSHKey(devbox.id);
290
- const fsModule = await import('fs');
291
- const pathModule = await import('path');
292
- const osModule = await import('os');
293
- const sshDir = pathModule.join(osModule.homedir(), '.runloop', 'ssh_keys');
412
+ const fsModule = await import("fs");
413
+ const pathModule = await import("path");
414
+ const osModule = await import("os");
415
+ const sshDir = pathModule.join(osModule.homedir(), ".runloop", "ssh_keys");
294
416
  fsModule.mkdirSync(sshDir, { recursive: true });
295
417
  const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
296
- fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, { mode: 0o600 });
297
- const sshUser = devbox.launch_parameters?.user_parameters?.username || 'user';
298
- const proxyCommand = 'openssl s_client -quiet -verify_quiet -servername %h -connect ssh.runloop.ai:443 2>/dev/null';
299
- global.__sshCommand = {
418
+ fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, {
419
+ mode: 0o600,
420
+ });
421
+ const sshUser = devbox.launch_parameters?.user_parameters?.username || "user";
422
+ const env = process.env.RUNLOOP_ENV?.toLowerCase();
423
+ const sshHost = env === "dev" ? "ssh.runloop.pro" : "ssh.runloop.ai";
424
+ const proxyCommand = `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
425
+ const sshConfig = {
300
426
  keyPath,
301
427
  proxyCommand,
302
428
  sshUser,
303
429
  url: sshKey.url,
304
- devboxName: devbox.name || devbox.id
430
+ devboxId: devbox.id,
431
+ devboxName: devbox.name || devbox.id,
305
432
  };
306
- exit();
433
+ // Notify parent that SSH is requested
434
+ if (onSSHRequest) {
435
+ onSSHRequest(sshConfig);
436
+ exit();
437
+ }
438
+ else {
439
+ setOperationError(new Error("SSH session handler not configured"));
440
+ }
307
441
  break;
308
- case 'logs':
442
+ case "logs":
309
443
  const logsResult = await client.devboxes.logs.list(devbox.id);
310
444
  if (logsResult.logs.length === 0) {
311
- setOperationResult('No logs available for this devbox.');
445
+ setOperationResult("No logs available for this devbox.");
312
446
  }
313
447
  else {
314
- logsResult.__customRender = 'logs';
448
+ logsResult.__customRender = "logs";
315
449
  logsResult.__logs = logsResult.logs;
316
450
  logsResult.__totalCount = logsResult.logs.length;
317
451
  setOperationResult(logsResult);
318
452
  }
319
453
  break;
320
- case 'tunnel':
454
+ case "tunnel":
321
455
  const port = parseInt(operationInput);
322
456
  if (isNaN(port) || port < 1 || port > 65535) {
323
- setOperationError(new Error('Invalid port number. Please enter a port between 1 and 65535.'));
457
+ setOperationError(new Error("Invalid port number. Please enter a port between 1 and 65535."));
324
458
  }
325
459
  else {
326
- const tunnel = await client.devboxes.createTunnel(devbox.id, { port });
460
+ const tunnel = await client.devboxes.createTunnel(devbox.id, {
461
+ port,
462
+ });
327
463
  setOperationResult(`Tunnel created!\n\n` +
328
464
  `Local Port: ${port}\n` +
329
465
  `Public URL: ${tunnel.url}\n\n` +
330
466
  `You can now access port ${port} on the devbox via:\n${tunnel.url}`);
331
467
  }
332
468
  break;
333
- case 'suspend':
469
+ case "suspend":
334
470
  await client.devboxes.suspend(devbox.id);
335
471
  setOperationResult(`Devbox ${devbox.id} suspended successfully`);
336
472
  break;
337
- case 'resume':
473
+ case "resume":
338
474
  await client.devboxes.resume(devbox.id);
339
475
  setOperationResult(`Devbox ${devbox.id} resumed successfully`);
340
476
  break;
341
- case 'delete':
477
+ case "delete":
342
478
  await client.devboxes.shutdown(devbox.id);
343
479
  setOperationResult(`Devbox ${devbox.id} shut down successfully`);
344
480
  break;
@@ -351,18 +487,20 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
351
487
  setLoading(false);
352
488
  }
353
489
  };
354
- const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
490
+ const operationLabel = operations.find((o) => o.key === executingOperation)?.label || "Operation";
355
491
  // Operation result display
356
492
  if (operationResult || operationError) {
357
493
  // Check for custom exec rendering
358
- if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
359
- const command = operationResult.command || '';
360
- const stdout = operationResult.stdout || '';
361
- const stderr = operationResult.stderr || '';
494
+ if (operationResult &&
495
+ typeof operationResult === "object" &&
496
+ operationResult.__customRender === "exec") {
497
+ const command = operationResult.command || "";
498
+ const stdout = operationResult.stdout || "";
499
+ const stderr = operationResult.stderr || "";
362
500
  const exitCode = operationResult.exitCode;
363
- const stdoutLines = stdout ? stdout.split('\n') : [];
364
- const stderrLines = stderr ? stderr.split('\n') : [];
365
- const allLines = [...stdoutLines, ...stderrLines].filter(line => line !== '');
501
+ const stdoutLines = stdout ? stdout.split("\n") : [];
502
+ const stderrLines = stderr ? stderr.split("\n") : [];
503
+ const allLines = [...stdoutLines, ...stderrLines].filter((line) => line !== "");
366
504
  const terminalHeight = stdout?.rows || 30;
367
505
  const viewportHeight = Math.max(10, terminalHeight - 15);
368
506
  const maxScroll = Math.max(0, allLines.length - viewportHeight);
@@ -370,16 +508,21 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
370
508
  const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
371
509
  const hasMore = actualScroll + viewportHeight < allLines.length;
372
510
  const hasLess = actualScroll > 0;
373
- const exitCodeColor = exitCode === 0 ? 'green' : 'red';
374
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: 'Execute Command', active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: "white", children: command })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "Exit Code: " }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: "gray", dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
511
+ const exitCodeColor = exitCode === 0 ? colors.success : colors.error;
512
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
513
+ ...breadcrumbItems,
514
+ { label: "Execute Command", active: true },
515
+ ] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Command:"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.text, children: command })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Exit Code:", " "] }), _jsx(Text, { color: exitCodeColor, bold: true, children: exitCode })] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [allLines.length === 0 && (_jsx(Text, { color: colors.textDim, dimColor: true, children: "No output" })), visibleLines.map((line, index) => {
375
516
  const actualIndex = actualScroll + index;
376
517
  const isStderr = actualIndex >= stdoutLines.length;
377
- const lineColor = isStderr ? 'red' : 'white';
518
+ const lineColor = isStderr ? colors.error : colors.text;
378
519
  return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
379
- }), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " lines" }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of ", allLines.length] })] })), stdout && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "green", dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "red", dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
520
+ }), hasLess && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.primary, children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { marginTop: hasLess ? 0 : 1, children: _jsxs(Text, { color: colors.primary, children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "lines"] }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of", " ", allLines.length] })] })), stdout && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.success, dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.error, dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
380
521
  }
381
522
  // Check for custom logs rendering
382
- if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
523
+ if (operationResult &&
524
+ typeof operationResult === "object" &&
525
+ operationResult.__customRender === "logs") {
383
526
  const logs = operationResult.__logs || [];
384
527
  const totalCount = operationResult.__totalCount || 0;
385
528
  const terminalHeight = stdout?.rows || 30;
@@ -390,71 +533,86 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
390
533
  const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
391
534
  const hasMore = actualScroll + viewportHeight < logs.length;
392
535
  const hasLess = actualScroll > 0;
393
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: 'Logs', active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [visibleLogs.map((log, index) => {
536
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: "Logs", active: true }] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: [visibleLogs.map((log, index) => {
394
537
  const time = new Date(log.timestamp_ms).toLocaleTimeString();
395
- const level = log.level ? log.level[0].toUpperCase() : 'I';
396
- const source = log.source ? log.source.substring(0, 8) : 'exec';
397
- const fullMessage = log.message || '';
398
- const cmd = log.cmd ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? '...' : ''}] ` : '';
399
- const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
400
- let levelColor = 'gray';
401
- if (level === 'E')
402
- levelColor = 'red';
403
- else if (level === 'W')
404
- levelColor = 'yellow';
405
- else if (level === 'I')
406
- levelColor = 'cyan';
538
+ const level = log.level ? log.level[0].toUpperCase() : "I";
539
+ const source = log.source ? log.source.substring(0, 8) : "exec";
540
+ const fullMessage = log.message || "";
541
+ const cmd = log.cmd
542
+ ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? "..." : ""}] `
543
+ : "";
544
+ const exitCode = log.exit_code !== null && log.exit_code !== undefined
545
+ ? `(${log.exit_code}) `
546
+ : "";
547
+ let levelColor = colors.textDim;
548
+ if (level === "E")
549
+ levelColor = colors.error;
550
+ else if (level === "W")
551
+ levelColor = colors.warning;
552
+ else if (level === "I")
553
+ levelColor = colors.primary;
407
554
  if (logsWrapMode) {
408
- return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: fullMessage })] }, index));
555
+ return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: colors.warning, children: exitCode }), cmd && (_jsx(Text, { color: colors.info, dimColor: true, children: cmd })), _jsx(Text, { children: fullMessage })] }, index));
409
556
  }
410
557
  else {
411
558
  const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
412
559
  const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
413
560
  const truncatedMessage = fullMessage.length > availableMessageWidth
414
- ? fullMessage.substring(0, availableMessageWidth - 3) + '...'
561
+ ? fullMessage.substring(0, availableMessageWidth - 3) +
562
+ "..."
415
563
  : fullMessage;
416
- return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: "gray", dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: "yellow", children: exitCode }), cmd && _jsx(Text, { color: "blue", dimColor: true, children: cmd }), _jsx(Text, { children: truncatedMessage })] }, index));
564
+ return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: time }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: true, children: level }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["/", source] }), _jsx(Text, { children: " " }), exitCode && _jsx(Text, { color: colors.warning, children: exitCode }), cmd && (_jsx(Text, { color: colors.info, dimColor: true, children: cmd })), _jsx(Text, { children: truncatedMessage })] }, index));
417
565
  }
418
- }), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: "cyan", children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.hamburger, " ", totalCount] }), _jsx(Text, { color: "gray", dimColor: true, children: " total logs" }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of ", logs.length] }), _jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: logsWrapMode ? 'green' : 'gray', bold: logsWrapMode, children: logsWrapMode ? 'Wrap: ON' : 'Wrap: OFF' }), copyStatus && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", dimColor: true, children: " \u2022 " }), _jsx(Text, { color: "green", bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
566
+ }), hasLess && (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: [figures.arrowUp, " More above"] }) })), hasMore && (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: [figures.arrowDown, " More below"] }) }))] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of", " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
419
567
  }
420
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _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" }) })] }));
568
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _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" }) })] }));
421
569
  }
422
570
  // Operation input mode
423
571
  if (executingOperation && devbox) {
424
- const needsInput = executingOperation === 'exec' ||
425
- executingOperation === 'upload' ||
426
- executingOperation === 'snapshot' ||
427
- executingOperation === 'tunnel';
572
+ const needsInput = executingOperation === "exec" ||
573
+ executingOperation === "upload" ||
574
+ executingOperation === "snapshot" ||
575
+ executingOperation === "tunnel";
428
576
  if (loading) {
429
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
577
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
578
+ ...breadcrumbItems,
579
+ { label: operationLabel, active: true },
580
+ ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
430
581
  }
431
582
  if (!needsInput) {
432
583
  const messages = {
433
- ssh: 'Creating SSH key...',
434
- logs: 'Fetching logs...',
435
- suspend: 'Suspending devbox...',
436
- resume: 'Resuming devbox...',
437
- delete: 'Shutting down devbox...',
584
+ ssh: "Creating SSH key...",
585
+ logs: "Fetching logs...",
586
+ suspend: "Suspending devbox...",
587
+ resume: "Resuming devbox...",
588
+ delete: "Shutting down devbox...",
438
589
  };
439
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
590
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
591
+ ...breadcrumbItems,
592
+ { label: operationLabel, active: true },
593
+ ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
440
594
  }
441
595
  const prompts = {
442
- exec: 'Command to execute:',
443
- upload: 'File path to upload:',
444
- snapshot: 'Snapshot name (optional):',
445
- tunnel: 'Port number to expose:',
596
+ exec: "Command to execute:",
597
+ upload: "File path to upload:",
598
+ snapshot: "Snapshot name (optional):",
599
+ tunnel: "Port number to expose:",
446
600
  };
447
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "cyan", bold: true, children: devbox.name || devbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: "gray", children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === 'exec'
448
- ? 'ls -la'
449
- : executingOperation === 'upload'
450
- ? '/path/to/file'
451
- : executingOperation === 'tunnel'
452
- ? '8080'
453
- : 'my-snapshot' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
601
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: devbox.name || devbox.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [prompts[executingOperation], " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: executingOperation === "exec"
602
+ ? "ls -la"
603
+ : executingOperation === "upload"
604
+ ? "/path/to/file"
605
+ : executingOperation === "tunnel"
606
+ ? "8080"
607
+ : "my-snapshot" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
608
+ }
609
+ // Operations selection mode - only show if not skipping
610
+ if (!skipOperationsMenu || !executingOperation) {
611
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
612
+ const isSelected = index === selectedOperation;
613
+ 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));
614
+ }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
454
615
  }
455
- // Operations selection mode
456
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
457
- const isSelected = index === selectedOperation;
458
- return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: [isSelected ? figures.pointer : ' ', " "] }), _jsxs(Text, { color: isSelected ? op.color : 'gray', bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: "gray", dimColor: true, children: [" [", op.shortcut, "]"] })] }, op.key));
459
- }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
616
+ // If skipOperationsMenu is true and executingOperation is set, show loading while it executes
617
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
460
618
  };