@runloop/rl-cli 0.0.1 → 0.0.3

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.
@@ -1,10 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text, useInput, useStdout } from 'ink';
3
+ import { Box, Text, useInput, useStdout, useApp } from 'ink';
4
4
  import figures from 'figures';
5
5
  import { getClient } from '../../utils/client.js';
6
- import { Header } from '../../components/Header.js';
7
- import { Banner } from '../../components/Banner.js';
8
6
  import { SpinnerComponent } from '../../components/Spinner.js';
9
7
  import { ErrorMessage } from '../../components/ErrorMessage.js';
10
8
  import { StatusBadge } from '../../components/StatusBadge.js';
@@ -33,8 +31,9 @@ const formatTimeAgo = (timestamp) => {
33
31
  const years = Math.floor(months / 12);
34
32
  return `${years}y ago`;
35
33
  };
36
- const ListSnapshotsUI = ({ devboxId }) => {
34
+ const ListSnapshotsUI = ({ devboxId, onBack, onExit }) => {
37
35
  const { stdout } = useStdout();
36
+ const { exit: inkExit } = useApp();
38
37
  const [loading, setLoading] = React.useState(true);
39
38
  const [snapshots, setSnapshots] = React.useState([]);
40
39
  const [error, setError] = React.useState(null);
@@ -89,8 +88,16 @@ const ListSnapshotsUI = ({ devboxId }) => {
89
88
  setCurrentPage(currentPage - 1);
90
89
  setSelectedIndex(0);
91
90
  }
92
- else if (input === 'q') {
93
- process.exit(0);
91
+ else if (key.escape) {
92
+ if (onBack) {
93
+ onBack();
94
+ }
95
+ else if (onExit) {
96
+ onExit();
97
+ }
98
+ else {
99
+ inkExit();
100
+ }
94
101
  }
95
102
  });
96
103
  const totalPages = Math.ceil(snapshots.length / PAGE_SIZE);
@@ -99,17 +106,19 @@ const ListSnapshotsUI = ({ devboxId }) => {
99
106
  const currentSnapshots = snapshots.slice(startIndex, endIndex);
100
107
  const ready = snapshots.filter((s) => s.status === 'ready').length;
101
108
  const pending = snapshots.filter((s) => s.status !== 'ready').length;
102
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Breadcrumb, { items: [
109
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
103
110
  { label: 'Snapshots', active: !devboxId },
104
111
  ...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
105
- ] }), _jsx(Header, { title: "Snapshots", subtitle: devboxId ? `Filtering by devbox: ${devboxId}` : undefined }), loading && _jsx(SpinnerComponent, { message: "Loading snapshots..." }), !loading && !error && snapshots.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No snapshots found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln snapshot create <devbox-id>" })] })), !loading && !error && snapshots.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", ready] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", pending] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", snapshots.length, snapshots.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: currentSnapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, columns: [
112
+ ] }), loading && _jsx(SpinnerComponent, { message: "Loading snapshots..." }), !loading && !error && snapshots.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No snapshots found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln snapshot create <devbox-id>" })] })), !loading && !error && snapshots.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", ready] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", pending] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", snapshots.length, snapshots.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: currentSnapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, columns: [
106
113
  createComponentColumn('status', 'Status', (snapshot) => _jsx(StatusBadge, { status: snapshot.status, showText: false }), { width: 2 }),
107
114
  createTextColumn('id', 'ID', (snapshot) => showFullId ? snapshot.id : snapshot.id.slice(0, 13), { width: showFullId ? idWidth : 15, color: 'gray', dimColor: true, bold: false }),
108
115
  createTextColumn('name', 'Name', (snapshot) => snapshot.name || '(unnamed)', { width: nameWidth }),
109
116
  createTextColumn('devbox', 'Devbox', (snapshot) => snapshot.devbox_id || '', { width: devboxWidth, color: 'cyan', dimColor: true, bold: false, visible: showDevboxId }),
110
117
  createTextColumn('created', 'Created', (snapshot) => snapshot.created_at ? formatTimeAgo(new Date(snapshot.created_at).getTime()) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
111
- ] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \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 snapshots", error: error })] }));
118
+ ] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \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 snapshots", error: error })] }));
112
119
  };
120
+ // Export the UI component for use in the main menu
121
+ export { ListSnapshotsUI };
113
122
  export async function listSnapshots(options) {
114
123
  const executor = createExecutor(options);
115
124
  await executor.executeList(async () => {
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import figures from 'figures';
4
+ import chalk from 'chalk';
5
+ export const ActionsPopup = ({ devbox, operations, selectedOperation, onClose, }) => {
6
+ // Calculate the maximum width needed
7
+ const maxLabelLength = Math.max(...operations.map(op => op.label.length));
8
+ const contentWidth = maxLabelLength + 12; // Content + icon + pointer + shortcuts
9
+ // Strip ANSI codes to get real length, then pad
10
+ const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, '');
11
+ const bgLine = (content) => {
12
+ const cleanLength = stripAnsi(content).length;
13
+ const padding = Math.max(0, contentWidth - cleanLength);
14
+ return chalk.bgBlack(content + ' '.repeat(padding));
15
+ };
16
+ // Render all lines with background
17
+ const lines = [
18
+ bgLine(chalk.cyan.bold(` ${figures.play} Quick Actions`)),
19
+ chalk.bgBlack(' '.repeat(contentWidth)),
20
+ ...operations.map((op, index) => {
21
+ const isSelected = index === selectedOperation;
22
+ const pointer = isSelected ? figures.pointer : ' ';
23
+ const content = ` ${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
24
+ let styled;
25
+ if (isSelected) {
26
+ const colorFn = chalk[op.color];
27
+ styled = typeof colorFn === 'function' ? colorFn.bold(content) : chalk.white.bold(content);
28
+ }
29
+ else {
30
+ styled = chalk.gray(content);
31
+ }
32
+ return bgLine(styled);
33
+ }),
34
+ chalk.bgBlack(' '.repeat(contentWidth)),
35
+ bgLine(chalk.gray.dim(` ${figures.arrowUp}${figures.arrowDown} Nav • [Enter]`)),
36
+ bgLine(chalk.gray.dim(` [Esc] Close`)),
37
+ ];
38
+ // Draw custom border with background to fill gaps
39
+ const borderTop = chalk.cyan('╭' + '─'.repeat(contentWidth) + '╮');
40
+ const borderBottom = chalk.cyan('╰' + '─'.repeat(contentWidth) + '╯');
41
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), lines.map((line, i) => (_jsxs(Text, { children: [chalk.cyan('│'), line, chalk.cyan('│')] }, i))), _jsx(Text, { children: borderBottom })] }) }));
42
+ };
@@ -1,11 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- import { Box, Text } from 'ink';
3
+ import { Box } from 'ink';
4
+ import BigText from 'ink-big-text';
4
5
  import Gradient from 'ink-gradient';
5
6
  export const Banner = React.memo(() => {
6
- return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(Gradient, { colors: ['#0a4d3a', '#e5f1ed'], children: _jsx(Text, { bold: true, children: `
7
- ╦═╗╦ ╦╔╗╔╦ ╔═╗╔═╗╔═╗
8
- ╠╦╝║ ║║║║║ ║ ║║ ║╠═╝
9
- ╩╚═╚═╝╝╚╝╩═╝╚═╝╚═╝╩ .ai
10
- ` }) }) }));
7
+ return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", children: _jsx(Gradient, { name: "vice", children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
11
8
  });
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { Box, Text } from 'ink';
4
- import figures from 'figures';
5
4
  export const Breadcrumb = React.memo(({ items }) => {
6
- const baseUrl = process.env.RUNLOOP_BASE_URL;
7
- const isDevEnvironment = baseUrl && baseUrl !== 'https://api.runloop.ai';
8
- return (_jsxs(Box, { marginBottom: 1, paddingX: 1, paddingY: 0, children: [_jsx(Text, { color: "green", dimColor: true, bold: true, children: "RL" }), isDevEnvironment && _jsx(Text, { color: "redBright", bold: true, children: " (dev)" }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowRight, " "] }), items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? 'cyan' : 'gray', bold: item.active, dimColor: !item.active, children: item.label }), index < items.length - 1 && (_jsx(Text, { color: "gray", dimColor: true, children: " / " }))] }, index)))] }));
5
+ const env = process.env.RUNLOOP_ENV?.toLowerCase();
6
+ const isDevEnvironment = env === 'dev';
7
+ return (_jsx(Box, { marginBottom: 1, paddingX: 2, paddingY: 0, children: _jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: "cyan", bold: true, children: "rl" }), isDevEnvironment && _jsx(Text, { color: "redBright", bold: true, children: " (dev)" }), _jsx(Text, { color: "gray", dimColor: true, children: " \u203A " }), items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? 'white' : 'gray', bold: item.active, dimColor: !item.active, children: item.label }), index < items.length - 1 && (_jsx(Text, { color: "gray", dimColor: true, children: " \u203A " }))] }, index)))] }) }));
9
8
  });
@@ -0,0 +1,481 @@
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';
12
+ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
13
+ { label: 'Devboxes' },
14
+ { label: devbox.name || devbox.id, active: true }
15
+ ], initialOperation, initialOperationIndex = 0, skipOperationsMenu = false, onSSHRequest, }) => {
16
+ const { exit } = useApp();
17
+ const { stdout } = useStdout();
18
+ const [loading, setLoading] = React.useState(false);
19
+ const [selectedOperation, setSelectedOperation] = React.useState(initialOperationIndex);
20
+ const [executingOperation, setExecutingOperation] = React.useState(initialOperation || null);
21
+ const [operationInput, setOperationInput] = React.useState('');
22
+ const [operationResult, setOperationResult] = React.useState(null);
23
+ const [operationError, setOperationError] = React.useState(null);
24
+ const [logsWrapMode, setLogsWrapMode] = React.useState(false);
25
+ const [logsScroll, setLogsScroll] = React.useState(0);
26
+ const [execScroll, setExecScroll] = React.useState(0);
27
+ const [copyStatus, setCopyStatus] = React.useState(null);
28
+ 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' },
38
+ ];
39
+ // 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;
57
+ // Auto-execute operations that don't need input
58
+ React.useEffect(() => {
59
+ const autoExecuteOps = ['delete', 'ssh', 'logs', 'suspend', 'resume'];
60
+ if (executingOperation && autoExecuteOps.includes(executingOperation) && !loading && devbox) {
61
+ executeOperation();
62
+ }
63
+ }, [executingOperation]);
64
+ useInput((input, key) => {
65
+ // Handle operation input mode
66
+ if (executingOperation && !operationResult && !operationError) {
67
+ if (key.return && operationInput.trim()) {
68
+ executeOperation();
69
+ }
70
+ else if (input === 'q' || key.escape) {
71
+ console.clear();
72
+ setExecutingOperation(null);
73
+ setOperationInput('');
74
+ }
75
+ return;
76
+ }
77
+ // Handle operation result display
78
+ if (operationResult || operationError) {
79
+ if (input === 'q' || key.escape || key.return) {
80
+ console.clear();
81
+ // If skipOperationsMenu is true, go back to parent instead of operations menu
82
+ if (skipOperationsMenu) {
83
+ onBack();
84
+ }
85
+ else {
86
+ setOperationResult(null);
87
+ setOperationError(null);
88
+ setExecutingOperation(null);
89
+ setOperationInput('');
90
+ setLogsWrapMode(true);
91
+ setLogsScroll(0);
92
+ setExecScroll(0);
93
+ setCopyStatus(null);
94
+ }
95
+ }
96
+ else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
97
+ setExecScroll(Math.max(0, execScroll - 1));
98
+ }
99
+ else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
100
+ setExecScroll(execScroll + 1);
101
+ }
102
+ else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
103
+ setExecScroll(Math.max(0, execScroll - 10));
104
+ }
105
+ else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
106
+ setExecScroll(execScroll + 10);
107
+ }
108
+ else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
109
+ setExecScroll(0);
110
+ }
111
+ else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
112
+ const lines = [...(operationResult.stdout || '').split('\n'), ...(operationResult.stderr || '').split('\n')];
113
+ const terminalHeight = stdout?.rows || 30;
114
+ const viewportHeight = Math.max(10, terminalHeight - 15);
115
+ const maxScroll = Math.max(0, lines.length - viewportHeight);
116
+ setExecScroll(maxScroll);
117
+ }
118
+ else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
119
+ // Copy exec output to clipboard
120
+ const output = (operationResult.stdout || '') + (operationResult.stderr || '');
121
+ const copyToClipboard = async (text) => {
122
+ const { spawn } = await import('child_process');
123
+ const platform = process.platform;
124
+ let command;
125
+ let args;
126
+ if (platform === 'darwin') {
127
+ command = 'pbcopy';
128
+ args = [];
129
+ }
130
+ else if (platform === 'win32') {
131
+ command = 'clip';
132
+ args = [];
133
+ }
134
+ else {
135
+ command = 'xclip';
136
+ args = ['-selection', 'clipboard'];
137
+ }
138
+ const proc = spawn(command, args);
139
+ proc.stdin.write(text);
140
+ proc.stdin.end();
141
+ proc.on('exit', (code) => {
142
+ if (code === 0) {
143
+ setCopyStatus('Copied to clipboard!');
144
+ setTimeout(() => setCopyStatus(null), 2000);
145
+ }
146
+ else {
147
+ setCopyStatus('Failed to copy');
148
+ setTimeout(() => setCopyStatus(null), 2000);
149
+ }
150
+ });
151
+ proc.on('error', () => {
152
+ setCopyStatus('Copy not supported');
153
+ setTimeout(() => setCopyStatus(null), 2000);
154
+ });
155
+ };
156
+ copyToClipboard(output);
157
+ }
158
+ else if ((key.upArrow || input === 'k') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
159
+ setLogsScroll(Math.max(0, logsScroll - 1));
160
+ }
161
+ else if ((key.downArrow || input === 'j') && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
162
+ setLogsScroll(logsScroll + 1);
163
+ }
164
+ else if (key.pageUp && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
165
+ setLogsScroll(Math.max(0, logsScroll - 10));
166
+ }
167
+ else if (key.pageDown && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
168
+ setLogsScroll(logsScroll + 10);
169
+ }
170
+ else if (input === 'g' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
171
+ setLogsScroll(0);
172
+ }
173
+ else if (input === 'G' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
174
+ const logs = operationResult.__logs || [];
175
+ const terminalHeight = stdout?.rows || 30;
176
+ const viewportHeight = Math.max(10, terminalHeight - 10);
177
+ const maxScroll = Math.max(0, logs.length - viewportHeight);
178
+ setLogsScroll(maxScroll);
179
+ }
180
+ else if (input === 'w' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
181
+ setLogsWrapMode(!logsWrapMode);
182
+ }
183
+ else if (input === 'c' && operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
184
+ // Copy logs to clipboard
185
+ const logs = operationResult.__logs || [];
186
+ const logsText = logs.map((log) => {
187
+ const time = new Date(log.timestamp_ms).toLocaleString();
188
+ const level = log.level || 'INFO';
189
+ const source = log.source || 'exec';
190
+ const message = log.message || '';
191
+ const cmd = log.cmd ? `[${log.cmd}] ` : '';
192
+ const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
193
+ return `${time} ${level}/${source} ${exitCode}${cmd}${message}`;
194
+ }).join('\n');
195
+ const copyToClipboard = async (text) => {
196
+ const { spawn } = await import('child_process');
197
+ const platform = process.platform;
198
+ let command;
199
+ let args;
200
+ if (platform === 'darwin') {
201
+ command = 'pbcopy';
202
+ args = [];
203
+ }
204
+ else if (platform === 'win32') {
205
+ command = 'clip';
206
+ args = [];
207
+ }
208
+ else {
209
+ command = 'xclip';
210
+ args = ['-selection', 'clipboard'];
211
+ }
212
+ const proc = spawn(command, args);
213
+ proc.stdin.write(text);
214
+ proc.stdin.end();
215
+ proc.on('exit', (code) => {
216
+ if (code === 0) {
217
+ setCopyStatus('Copied to clipboard!');
218
+ setTimeout(() => setCopyStatus(null), 2000);
219
+ }
220
+ else {
221
+ setCopyStatus('Failed to copy');
222
+ setTimeout(() => setCopyStatus(null), 2000);
223
+ }
224
+ });
225
+ proc.on('error', () => {
226
+ setCopyStatus('Copy not supported');
227
+ setTimeout(() => setCopyStatus(null), 2000);
228
+ });
229
+ };
230
+ copyToClipboard(logsText);
231
+ }
232
+ return;
233
+ }
234
+ // Operations selection mode
235
+ if (input === 'q' || key.escape) {
236
+ console.clear();
237
+ onBack();
238
+ setSelectedOperation(0);
239
+ }
240
+ else if (key.upArrow && selectedOperation > 0) {
241
+ setSelectedOperation(selectedOperation - 1);
242
+ }
243
+ else if (key.downArrow && selectedOperation < operations.length - 1) {
244
+ setSelectedOperation(selectedOperation + 1);
245
+ }
246
+ else if (key.return) {
247
+ console.clear();
248
+ const op = operations[selectedOperation].key;
249
+ setExecutingOperation(op);
250
+ }
251
+ else if (input) {
252
+ // Check if input matches any operation shortcut
253
+ const matchedOp = operations.find(op => op.shortcut === input);
254
+ if (matchedOp) {
255
+ console.clear();
256
+ setExecutingOperation(matchedOp.key);
257
+ }
258
+ }
259
+ });
260
+ const executeOperation = async () => {
261
+ const client = getClient();
262
+ try {
263
+ setLoading(true);
264
+ switch (executingOperation) {
265
+ case 'exec':
266
+ const execResult = await client.devboxes.executeSync(devbox.id, {
267
+ command: operationInput,
268
+ });
269
+ // Format exec result for custom rendering
270
+ const formattedExecResult = {
271
+ __customRender: 'exec',
272
+ command: operationInput,
273
+ stdout: execResult.stdout || '',
274
+ stderr: execResult.stderr || '',
275
+ exitCode: execResult.exit_code ?? 0,
276
+ };
277
+ setOperationResult(formattedExecResult);
278
+ break;
279
+ case 'upload':
280
+ const fs = await import('fs');
281
+ const fileStream = fs.createReadStream(operationInput);
282
+ const filename = operationInput.split('/').pop() || 'file';
283
+ await client.devboxes.uploadFile(devbox.id, {
284
+ path: filename,
285
+ file: fileStream,
286
+ });
287
+ setOperationResult(`File ${filename} uploaded successfully`);
288
+ break;
289
+ case 'snapshot':
290
+ const snapshot = await client.devboxes.snapshotDisk(devbox.id, {
291
+ name: operationInput || `snapshot-${Date.now()}`,
292
+ });
293
+ setOperationResult(`Snapshot created: ${snapshot.id}`);
294
+ break;
295
+ case 'ssh':
296
+ const sshKey = await client.devboxes.createSSHKey(devbox.id);
297
+ const fsModule = await import('fs');
298
+ const pathModule = await import('path');
299
+ const osModule = await import('os');
300
+ const sshDir = pathModule.join(osModule.homedir(), '.runloop', 'ssh_keys');
301
+ fsModule.mkdirSync(sshDir, { recursive: true });
302
+ const keyPath = pathModule.join(sshDir, `${devbox.id}.pem`);
303
+ fsModule.writeFileSync(keyPath, sshKey.ssh_private_key, { mode: 0o600 });
304
+ const sshUser = devbox.launch_parameters?.user_parameters?.username || 'user';
305
+ const env = process.env.RUNLOOP_ENV?.toLowerCase();
306
+ const sshHost = env === 'dev' ? 'ssh.runloop.pro' : 'ssh.runloop.ai';
307
+ const proxyCommand = `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshHost}:443 2>/dev/null`;
308
+ const sshConfig = {
309
+ keyPath,
310
+ proxyCommand,
311
+ sshUser,
312
+ url: sshKey.url,
313
+ devboxId: devbox.id,
314
+ devboxName: devbox.name || devbox.id
315
+ };
316
+ // Notify parent that SSH is requested
317
+ if (onSSHRequest) {
318
+ onSSHRequest(sshConfig);
319
+ exit();
320
+ }
321
+ else {
322
+ setOperationError(new Error('SSH session handler not configured'));
323
+ }
324
+ break;
325
+ case 'logs':
326
+ const logsResult = await client.devboxes.logs.list(devbox.id);
327
+ if (logsResult.logs.length === 0) {
328
+ setOperationResult('No logs available for this devbox.');
329
+ }
330
+ else {
331
+ logsResult.__customRender = 'logs';
332
+ logsResult.__logs = logsResult.logs;
333
+ logsResult.__totalCount = logsResult.logs.length;
334
+ setOperationResult(logsResult);
335
+ }
336
+ break;
337
+ case 'tunnel':
338
+ const port = parseInt(operationInput);
339
+ if (isNaN(port) || port < 1 || port > 65535) {
340
+ setOperationError(new Error('Invalid port number. Please enter a port between 1 and 65535.'));
341
+ }
342
+ else {
343
+ const tunnel = await client.devboxes.createTunnel(devbox.id, { port });
344
+ setOperationResult(`Tunnel created!\n\n` +
345
+ `Local Port: ${port}\n` +
346
+ `Public URL: ${tunnel.url}\n\n` +
347
+ `You can now access port ${port} on the devbox via:\n${tunnel.url}`);
348
+ }
349
+ break;
350
+ case 'suspend':
351
+ await client.devboxes.suspend(devbox.id);
352
+ setOperationResult(`Devbox ${devbox.id} suspended successfully`);
353
+ break;
354
+ case 'resume':
355
+ await client.devboxes.resume(devbox.id);
356
+ setOperationResult(`Devbox ${devbox.id} resumed successfully`);
357
+ break;
358
+ case 'delete':
359
+ await client.devboxes.shutdown(devbox.id);
360
+ setOperationResult(`Devbox ${devbox.id} shut down successfully`);
361
+ break;
362
+ }
363
+ }
364
+ catch (err) {
365
+ setOperationError(err);
366
+ }
367
+ finally {
368
+ setLoading(false);
369
+ }
370
+ };
371
+ const operationLabel = operations.find((o) => o.key === executingOperation)?.label || 'Operation';
372
+ // Operation result display
373
+ if (operationResult || operationError) {
374
+ // Check for custom exec rendering
375
+ if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'exec') {
376
+ const command = operationResult.command || '';
377
+ const stdout = operationResult.stdout || '';
378
+ const stderr = operationResult.stderr || '';
379
+ const exitCode = operationResult.exitCode;
380
+ const stdoutLines = stdout ? stdout.split('\n') : [];
381
+ const stderrLines = stderr ? stderr.split('\n') : [];
382
+ const allLines = [...stdoutLines, ...stderrLines].filter(line => line !== '');
383
+ const terminalHeight = stdout?.rows || 30;
384
+ const viewportHeight = Math.max(10, terminalHeight - 15);
385
+ const maxScroll = Math.max(0, allLines.length - viewportHeight);
386
+ const actualScroll = Math.min(execScroll, maxScroll);
387
+ const visibleLines = allLines.slice(actualScroll, actualScroll + viewportHeight);
388
+ const hasMore = actualScroll + viewportHeight < allLines.length;
389
+ const hasLess = actualScroll > 0;
390
+ const exitCodeColor = exitCode === 0 ? 'green' : 'red';
391
+ 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) => {
392
+ const actualIndex = actualScroll + index;
393
+ const isStderr = actualIndex >= stdoutLines.length;
394
+ const lineColor = isStderr ? 'red' : 'white';
395
+ return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
396
+ }), 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"] }) })] }));
397
+ }
398
+ // Check for custom logs rendering
399
+ if (operationResult && typeof operationResult === 'object' && operationResult.__customRender === 'logs') {
400
+ const logs = operationResult.__logs || [];
401
+ const totalCount = operationResult.__totalCount || 0;
402
+ const terminalHeight = stdout?.rows || 30;
403
+ const terminalWidth = stdout?.columns || 120;
404
+ const viewportHeight = Math.max(10, terminalHeight - 10);
405
+ const maxScroll = Math.max(0, logs.length - viewportHeight);
406
+ const actualScroll = Math.min(logsScroll, maxScroll);
407
+ const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
408
+ const hasMore = actualScroll + viewportHeight < logs.length;
409
+ const hasLess = actualScroll > 0;
410
+ 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) => {
411
+ const time = new Date(log.timestamp_ms).toLocaleTimeString();
412
+ const level = log.level ? log.level[0].toUpperCase() : 'I';
413
+ const source = log.source ? log.source.substring(0, 8) : 'exec';
414
+ const fullMessage = log.message || '';
415
+ const cmd = log.cmd ? `[${log.cmd.substring(0, 40)}${log.cmd.length > 40 ? '...' : ''}] ` : '';
416
+ const exitCode = log.exit_code !== null && log.exit_code !== undefined ? `(${log.exit_code}) ` : '';
417
+ let levelColor = 'gray';
418
+ if (level === 'E')
419
+ levelColor = 'red';
420
+ else if (level === 'W')
421
+ levelColor = 'yellow';
422
+ else if (level === 'I')
423
+ levelColor = 'cyan';
424
+ if (logsWrapMode) {
425
+ 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));
426
+ }
427
+ else {
428
+ const metadataWidth = 11 + 1 + 1 + 1 + 8 + 1 + exitCode.length + cmd.length + 6;
429
+ const availableMessageWidth = Math.max(20, terminalWidth - metadataWidth);
430
+ const truncatedMessage = fullMessage.length > availableMessageWidth
431
+ ? fullMessage.substring(0, availableMessageWidth - 3) + '...'
432
+ : fullMessage;
433
+ 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));
434
+ }
435
+ }), 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"] }) })] }));
436
+ }
437
+ 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" }) })] }));
438
+ }
439
+ // Operation input mode
440
+ if (executingOperation && devbox) {
441
+ const needsInput = executingOperation === 'exec' ||
442
+ executingOperation === 'upload' ||
443
+ executingOperation === 'snapshot' ||
444
+ executingOperation === 'tunnel';
445
+ if (loading) {
446
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
447
+ }
448
+ if (!needsInput) {
449
+ const messages = {
450
+ ssh: 'Creating SSH key...',
451
+ logs: 'Fetching logs...',
452
+ suspend: 'Suspending devbox...',
453
+ resume: 'Resuming devbox...',
454
+ delete: 'Shutting down devbox...',
455
+ };
456
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || 'Please wait...' })] }));
457
+ }
458
+ const prompts = {
459
+ exec: 'Command to execute:',
460
+ upload: 'File path to upload:',
461
+ snapshot: 'Snapshot name (optional):',
462
+ tunnel: 'Port number to expose:',
463
+ };
464
+ 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'
465
+ ? 'ls -la'
466
+ : executingOperation === 'upload'
467
+ ? '/path/to/file'
468
+ : executingOperation === 'tunnel'
469
+ ? '8080'
470
+ : 'my-snapshot' }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
471
+ }
472
+ // Operations selection mode - only show if not skipping
473
+ if (!skipOperationsMenu || !executingOperation) {
474
+ 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) => {
475
+ const isSelected = index === selectedOperation;
476
+ 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));
477
+ }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022 [q] Back"] }) })] }));
478
+ }
479
+ // If skipOperationsMenu is true and executingOperation is set, show loading while it executes
480
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
481
+ };