@runloop/rl-cli 0.0.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/dist/cli.js +110 -0
  4. package/dist/commands/auth.js +27 -0
  5. package/dist/commands/blueprint/list.js +373 -0
  6. package/dist/commands/create.js +42 -0
  7. package/dist/commands/delete.js +34 -0
  8. package/dist/commands/devbox/create.js +45 -0
  9. package/dist/commands/devbox/delete.js +37 -0
  10. package/dist/commands/devbox/exec.js +35 -0
  11. package/dist/commands/devbox/list.js +302 -0
  12. package/dist/commands/devbox/upload.js +40 -0
  13. package/dist/commands/exec.js +35 -0
  14. package/dist/commands/list.js +59 -0
  15. package/dist/commands/snapshot/create.js +40 -0
  16. package/dist/commands/snapshot/delete.js +37 -0
  17. package/dist/commands/snapshot/list.js +122 -0
  18. package/dist/commands/upload.js +40 -0
  19. package/dist/components/Banner.js +11 -0
  20. package/dist/components/Breadcrumb.js +9 -0
  21. package/dist/components/DetailView.js +29 -0
  22. package/dist/components/DevboxCard.js +24 -0
  23. package/dist/components/DevboxCreatePage.js +370 -0
  24. package/dist/components/DevboxDetailPage.js +621 -0
  25. package/dist/components/ErrorMessage.js +6 -0
  26. package/dist/components/Header.js +6 -0
  27. package/dist/components/MetadataDisplay.js +24 -0
  28. package/dist/components/OperationsMenu.js +19 -0
  29. package/dist/components/Spinner.js +6 -0
  30. package/dist/components/StatusBadge.js +41 -0
  31. package/dist/components/SuccessMessage.js +6 -0
  32. package/dist/components/Table.example.js +55 -0
  33. package/dist/components/Table.js +54 -0
  34. package/dist/utils/CommandExecutor.js +115 -0
  35. package/dist/utils/client.js +13 -0
  36. package/dist/utils/config.js +17 -0
  37. package/dist/utils/output.js +115 -0
  38. package/package.json +69 -0
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Badge } from '@inkjs/ui';
4
+ import figures from 'figures';
5
+ // Generate color for each key based on hash
6
+ const getColorForKey = (key, index) => {
7
+ const colors = ['cyan', 'magenta', 'yellow', 'blue', 'green', 'red'];
8
+ return colors[index % colors.length];
9
+ };
10
+ export const MetadataDisplay = ({ metadata, title = 'Metadata', showBorder = false, selectedKey }) => {
11
+ const entries = Object.entries(metadata);
12
+ if (entries.length === 0) {
13
+ return null;
14
+ }
15
+ const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "#0a4d3a", bold: true, children: [figures.info, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
16
+ const color = getColorForKey(key, index);
17
+ const isSelected = selectedKey === key;
18
+ return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: "cyan", bold: true, children: [figures.pointer, " "] })), _jsx(Badge, { color: isSelected ? 'cyan' : color, children: `${key}: ${value}` })] }, key));
19
+ })] }));
20
+ if (showBorder) {
21
+ return (_jsx(Box, { borderStyle: "round", borderColor: "#0a4d3a", paddingX: 2, paddingY: 1, flexDirection: "column", children: content }));
22
+ }
23
+ return content;
24
+ };
@@ -0,0 +1,19 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import figures from 'figures';
4
+ /**
5
+ * Reusable operations menu component for detail pages
6
+ * Displays a list of available operations with keyboard navigation
7
+ */
8
+ export const OperationsMenu = ({ operations, selectedIndex, onNavigate, onSelect, onBack, additionalActions = [], }) => {
9
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
10
+ const isSelected = index === selectedIndex;
11
+ 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] })] }, op.key));
12
+ }) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Select \u2022", additionalActions.map((action) => ` [${action.key}] ${action.label} •`), ' ', "[q] Back"] }) })] }));
13
+ };
14
+ /**
15
+ * Helper to filter operations based on conditions
16
+ */
17
+ export function filterOperations(allOperations, condition) {
18
+ return allOperations.filter(condition);
19
+ }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ export const SpinnerComponent = ({ message }) => {
5
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] }));
6
+ };
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import figures from 'figures';
4
+ export const getStatusDisplay = (status) => {
5
+ if (!status) {
6
+ return { icon: figures.questionMarkPrefix, color: 'gray', text: 'UNKNOWN' };
7
+ }
8
+ switch (status) {
9
+ case 'running':
10
+ return { icon: figures.circleFilled, color: 'green', text: 'RUNNING' };
11
+ case 'provisioning':
12
+ return { icon: figures.hamburger, color: 'yellow', text: 'PROVISIONING' };
13
+ case 'initializing':
14
+ return { icon: figures.ellipsis, color: 'cyan', text: 'INITIALIZING' };
15
+ case 'suspended':
16
+ return { icon: figures.circleDotted, color: 'yellow', text: 'SUSPENDED' };
17
+ case 'failure':
18
+ return { icon: figures.cross, color: 'red', text: 'FAILED' };
19
+ case 'shutdown':
20
+ return { icon: figures.circle, color: 'gray', text: 'SHUTDOWN' };
21
+ case 'resuming':
22
+ return { icon: figures.ellipsis, color: 'cyan', text: 'RESUMING' };
23
+ case 'suspending':
24
+ return { icon: figures.ellipsis, color: 'yellow', text: 'SUSPENDING' };
25
+ case 'ready':
26
+ return { icon: figures.tick, color: 'green', text: 'READY' };
27
+ case 'build_complete':
28
+ case 'building_complete':
29
+ return { icon: figures.tick, color: 'green', text: 'COMPLETE' };
30
+ case 'building':
31
+ return { icon: figures.ellipsis, color: 'yellow', text: 'BUILDING' };
32
+ case 'build_failed':
33
+ return { icon: figures.cross, color: 'red', text: 'FAILED' };
34
+ default:
35
+ return { icon: figures.questionMarkPrefix, color: 'gray', text: status.toUpperCase() };
36
+ }
37
+ };
38
+ export const StatusBadge = ({ status, showText = true }) => {
39
+ const statusDisplay = getStatusDisplay(status);
40
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: statusDisplay.color, children: statusDisplay.icon }), showText && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: statusDisplay.color, children: statusDisplay.text })] }))] }));
41
+ };
@@ -0,0 +1,6 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import figures from 'figures';
4
+ export const SuccessMessage = ({ message, details, }) => {
5
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: "green", bold: true, children: [figures.tick, " ", message] }) }), details && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: details.split('\n').map((line, i) => (_jsx(Text, { color: "gray", dimColor: true, children: line }, i))) }))] }));
6
+ };
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { Table, createTextColumn, createComponentColumn } from './Table.js';
4
+ import { StatusBadge } from './StatusBadge.js';
5
+ import figures from 'figures';
6
+ function BlueprintsTable({ blueprints, selectedIndex, terminalWidth }) {
7
+ // Responsive column widths
8
+ const showDescription = terminalWidth >= 120;
9
+ const showFullId = terminalWidth >= 80;
10
+ return (_jsx(Table, { data: blueprints, keyExtractor: (bp) => bp.id, selectedIndex: selectedIndex, columns: [
11
+ // Status badge column
12
+ createComponentColumn('status', 'Status', (bp) => _jsx(StatusBadge, { status: bp.status, showText: false }), { width: 2 }),
13
+ // ID column (responsive)
14
+ createTextColumn('id', 'ID', (bp) => showFullId ? bp.id : bp.id.slice(0, 13), { width: showFullId ? 25 : 15, color: 'gray', dimColor: true, bold: false }),
15
+ // Name column
16
+ createTextColumn('name', 'Name', (bp) => bp.name || '(unnamed)', { width: 30 }),
17
+ // Description column (optional)
18
+ createTextColumn('description', 'Description', (bp) => bp.description || '', { width: 40, color: 'gray', dimColor: true, bold: false, visible: showDescription }),
19
+ // Created time column
20
+ createTextColumn('created', 'Created', (bp) => new Date(bp.created_at).toLocaleDateString(), { width: 15, color: 'gray', dimColor: true, bold: false }),
21
+ ], emptyState: _jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [figures.info, " No blueprints found"] }) }) }));
22
+ }
23
+ function SnapshotsTable({ snapshots, selectedIndex, terminalWidth }) {
24
+ // Responsive column widths
25
+ const showSize = terminalWidth >= 100;
26
+ const showFullId = terminalWidth >= 80;
27
+ return (_jsx(Table, { data: snapshots, keyExtractor: (snap) => snap.id, selectedIndex: selectedIndex, columns: [
28
+ // Status badge column
29
+ createComponentColumn('status', 'Status', (snap) => _jsx(StatusBadge, { status: snap.status, showText: false }), { width: 2 }),
30
+ // ID column (responsive)
31
+ createTextColumn('id', 'ID', (snap) => showFullId ? snap.id : snap.id.slice(0, 13), { width: showFullId ? 25 : 15, color: 'gray', dimColor: true, bold: false }),
32
+ // Name column
33
+ createTextColumn('name', 'Name', (snap) => snap.name || '(unnamed)', { width: 25 }),
34
+ // Devbox ID column
35
+ createTextColumn('devbox', 'Devbox', (snap) => snap.devbox_id.slice(0, 13), { width: 15, color: 'cyan', dimColor: true, bold: false }),
36
+ // Size column (optional)
37
+ createTextColumn('size', 'Size', (snap) => snap.size_gb ? `${snap.size_gb.toFixed(1)}GB` : '', { width: 10, color: 'yellow', dimColor: true, bold: false, visible: showSize }),
38
+ // Created time column
39
+ createTextColumn('created', 'Created', (snap) => new Date(snap.created_at).toLocaleDateString(), { width: 15, color: 'gray', dimColor: true, bold: false }),
40
+ ], emptyState: _jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [figures.info, " No snapshots found"] }) }) }));
41
+ }
42
+ // ============================================================================
43
+ // EXAMPLE 3: Custom Column with Complex Rendering
44
+ // ============================================================================
45
+ function CustomComplexColumn() {
46
+ const data = [
47
+ { id: '1', name: 'Item 1', tags: ['tag1', 'tag2'] },
48
+ { id: '2', name: 'Item 2', tags: ['tag3'] },
49
+ ];
50
+ return (_jsx(Table, { data: data, keyExtractor: (item) => item.id, selectedIndex: 0, columns: [
51
+ createTextColumn('name', 'Name', (item) => item.name, { width: 20 }),
52
+ // Custom component column with complex rendering
53
+ createComponentColumn('tags', 'Tags', (item, index, isSelected) => (_jsx(Box, { width: 30, children: _jsx(Text, { color: isSelected ? 'cyan' : 'blue', dimColor: true, children: item.tags.map(tag => `[${tag}]`).join(' ') }) })), { width: 30 }),
54
+ ] }));
55
+ }
@@ -0,0 +1,54 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import figures from 'figures';
5
+ /**
6
+ * Reusable table component for displaying lists of data with optional selection
7
+ * Designed to be responsive and work across devboxes, blueprints, and snapshots
8
+ */
9
+ export function Table({ data, columns, selectedIndex = -1, showSelection = true, emptyState, keyExtractor, title, }) {
10
+ if (data.length === 0 && emptyState) {
11
+ return _jsx(_Fragment, { children: emptyState });
12
+ }
13
+ // Filter visible columns
14
+ const visibleColumns = columns.filter(col => col.visible !== false);
15
+ return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color: "cyan", bold: true, children: ["\u256D\u2500 ", title, " ", '─'.repeat(Math.max(0, 10)), "\u256E"] }) })), _jsxs(Box, { flexDirection: "column", borderStyle: title ? 'single' : 'round', borderColor: "gray", paddingX: 1, children: [_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column) => (_jsx(Text, { bold: true, dimColor: true, children: column.label.slice(0, column.width).padEnd(column.width, ' ') }, `header-${column.key}`)))] }), data.map((row, index) => {
16
+ const isSelected = index === selectedIndex;
17
+ const rowKey = keyExtractor(row);
18
+ return (_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ? 'cyan' : 'gray', children: isSelected ? figures.pointer : ' ' }), _jsx(Text, { children: " " })] })), visibleColumns.map((column, colIndex) => (_jsx(React.Fragment, { children: column.render(row, index, isSelected) }, `${rowKey}-${column.key}`)))] }, rowKey));
19
+ })] })] }));
20
+ }
21
+ /**
22
+ * Helper function to create a simple text column
23
+ */
24
+ export function createTextColumn(key, label, getValue, options) {
25
+ return {
26
+ key,
27
+ label,
28
+ width: options?.width || 20,
29
+ visible: options?.visible,
30
+ render: (row, index, isSelected) => {
31
+ const value = getValue(row);
32
+ const width = options?.width || 20;
33
+ const color = options?.color || (isSelected ? 'white' : 'white');
34
+ const bold = options?.bold !== undefined ? options.bold : isSelected;
35
+ const dimColor = options?.dimColor || false;
36
+ // Pad the value to fill the full width
37
+ const truncated = value.slice(0, width);
38
+ const padded = truncated.padEnd(width, ' ');
39
+ return (_jsx(Text, { color: isSelected ? 'white' : color, bold: bold, dimColor: !isSelected && dimColor, inverse: isSelected, children: padded }));
40
+ },
41
+ };
42
+ }
43
+ /**
44
+ * Helper function to create a component column (for badges, icons, etc.)
45
+ */
46
+ export function createComponentColumn(key, label, renderComponent, options) {
47
+ return {
48
+ key,
49
+ label,
50
+ width: options?.width || 20,
51
+ visible: options?.visible,
52
+ render: renderComponent,
53
+ };
54
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared class for executing commands with different output formats
3
+ * Reduces code duplication across all command files
4
+ */
5
+ import { render } from 'ink';
6
+ import { getClient } from './client.js';
7
+ import { shouldUseNonInteractiveOutput, outputList, outputResult } from './output.js';
8
+ import YAML from 'yaml';
9
+ export class CommandExecutor {
10
+ options;
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ }
14
+ /**
15
+ * Execute a list command with automatic format handling
16
+ */
17
+ async executeList(fetchData, renderUI, limit = 10) {
18
+ if (shouldUseNonInteractiveOutput(this.options)) {
19
+ try {
20
+ const items = await fetchData();
21
+ // Limit results for non-interactive mode
22
+ const limitedItems = items.slice(0, limit);
23
+ outputList(limitedItems, this.options);
24
+ }
25
+ catch (err) {
26
+ this.handleError(err);
27
+ }
28
+ return;
29
+ }
30
+ // Interactive mode
31
+ console.clear();
32
+ const { waitUntilExit } = render(renderUI());
33
+ await waitUntilExit();
34
+ }
35
+ /**
36
+ * Execute a create/action command with automatic format handling
37
+ */
38
+ async executeAction(performAction, renderUI) {
39
+ if (shouldUseNonInteractiveOutput(this.options)) {
40
+ try {
41
+ const result = await performAction();
42
+ outputResult(result, this.options);
43
+ }
44
+ catch (err) {
45
+ this.handleError(err);
46
+ }
47
+ return;
48
+ }
49
+ // Interactive mode
50
+ console.clear();
51
+ const { waitUntilExit } = render(renderUI());
52
+ await waitUntilExit();
53
+ }
54
+ /**
55
+ * Execute a delete command with automatic format handling
56
+ */
57
+ async executeDelete(performDelete, id, renderUI) {
58
+ if (shouldUseNonInteractiveOutput(this.options)) {
59
+ try {
60
+ await performDelete();
61
+ outputResult({ id, status: 'deleted' }, this.options);
62
+ }
63
+ catch (err) {
64
+ this.handleError(err);
65
+ }
66
+ return;
67
+ }
68
+ // Interactive mode
69
+ const { waitUntilExit } = render(renderUI());
70
+ await waitUntilExit();
71
+ }
72
+ /**
73
+ * Fetch items from an async iterator with optional filtering and limits
74
+ */
75
+ async fetchFromIterator(iterator, options = {}) {
76
+ const { filter, limit = 100 } = options;
77
+ const items = [];
78
+ let count = 0;
79
+ for await (const item of iterator) {
80
+ if (filter && !filter(item)) {
81
+ continue;
82
+ }
83
+ items.push(item);
84
+ count++;
85
+ if (count >= limit) {
86
+ break;
87
+ }
88
+ }
89
+ return items;
90
+ }
91
+ /**
92
+ * Handle errors consistently across all commands
93
+ */
94
+ handleError(error) {
95
+ if (this.options.output === 'yaml') {
96
+ console.error(YAML.stringify({ error: error.message }));
97
+ }
98
+ else {
99
+ console.error(JSON.stringify({ error: error.message }, null, 2));
100
+ }
101
+ process.exit(1);
102
+ }
103
+ /**
104
+ * Get the client instance
105
+ */
106
+ getClient() {
107
+ return getClient();
108
+ }
109
+ }
110
+ /**
111
+ * Factory function to create a CommandExecutor
112
+ */
113
+ export function createExecutor(options = {}) {
114
+ return new CommandExecutor(options);
115
+ }
@@ -0,0 +1,13 @@
1
+ import Runloop from '@runloop/api-client';
2
+ import { getConfig } from './config.js';
3
+ export function getClient() {
4
+ const config = getConfig();
5
+ if (!config.apiKey) {
6
+ throw new Error('API key not configured. Run: rln auth');
7
+ }
8
+ return new Runloop({
9
+ bearerToken: config.apiKey,
10
+ timeout: 10000, // 10 seconds instead of default 30 seconds
11
+ maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
12
+ });
13
+ }
@@ -0,0 +1,17 @@
1
+ import Conf from 'conf';
2
+ const config = new Conf({
3
+ projectName: 'runloop-cli',
4
+ });
5
+ export function getConfig() {
6
+ // Check environment variable first, then fall back to stored config
7
+ const apiKey = process.env.RUNLOOP_API_KEY || config.get('apiKey');
8
+ return {
9
+ apiKey,
10
+ };
11
+ }
12
+ export function setApiKey(apiKey) {
13
+ config.set('apiKey', apiKey);
14
+ }
15
+ export function clearConfig() {
16
+ config.clear();
17
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Utility for handling different output formats across CLI commands
3
+ */
4
+ import YAML from 'yaml';
5
+ /**
6
+ * Check if the command should use non-interactive output
7
+ */
8
+ export function shouldUseNonInteractiveOutput(options) {
9
+ return !!options.output;
10
+ }
11
+ /**
12
+ * Output data in the specified format
13
+ */
14
+ export function outputData(data, format = 'json') {
15
+ if (format === 'json') {
16
+ console.log(JSON.stringify(data, null, 2));
17
+ return;
18
+ }
19
+ if (format === 'yaml') {
20
+ console.log(YAML.stringify(data));
21
+ return;
22
+ }
23
+ if (format === 'text') {
24
+ // Simple text output
25
+ if (Array.isArray(data)) {
26
+ // For lists of complex objects, just output IDs
27
+ data.forEach((item) => {
28
+ if (typeof item === 'object' && item !== null && 'id' in item) {
29
+ console.log(item.id);
30
+ }
31
+ else {
32
+ console.log(formatTextOutput(item));
33
+ }
34
+ });
35
+ }
36
+ else {
37
+ console.log(formatTextOutput(data));
38
+ }
39
+ return;
40
+ }
41
+ console.error(`Unknown output format: ${format}`);
42
+ process.exit(1);
43
+ }
44
+ /**
45
+ * Format a single item as text output
46
+ */
47
+ function formatTextOutput(item) {
48
+ if (typeof item === 'string') {
49
+ return item;
50
+ }
51
+ // For objects, create a simple key: value format
52
+ const lines = [];
53
+ for (const [key, value] of Object.entries(item)) {
54
+ if (value !== null && value !== undefined) {
55
+ lines.push(`${key}: ${value}`);
56
+ }
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+ /**
61
+ * Output a single result (for create, delete, etc)
62
+ */
63
+ export function outputResult(result, options, successMessage) {
64
+ if (shouldUseNonInteractiveOutput(options)) {
65
+ outputData(result, options.output);
66
+ return;
67
+ }
68
+ // Interactive mode - print success message
69
+ if (successMessage) {
70
+ console.log(successMessage);
71
+ }
72
+ }
73
+ /**
74
+ * Output a list of items (for list commands)
75
+ */
76
+ export function outputList(items, options) {
77
+ if (shouldUseNonInteractiveOutput(options)) {
78
+ outputData(items, options.output);
79
+ }
80
+ }
81
+ /**
82
+ * Handle errors in both interactive and non-interactive modes
83
+ */
84
+ export function outputError(error, options) {
85
+ if (shouldUseNonInteractiveOutput(options)) {
86
+ if (options.output === 'json') {
87
+ console.error(JSON.stringify({ error: error.message }, null, 2));
88
+ }
89
+ else if (options.output === 'yaml') {
90
+ console.error(YAML.stringify({ error: error.message }));
91
+ }
92
+ else {
93
+ console.error(`Error: ${error.message}`);
94
+ }
95
+ process.exit(1);
96
+ }
97
+ // Let interactive UI handle the error
98
+ throw error;
99
+ }
100
+ /**
101
+ * Validate output format option
102
+ */
103
+ export function validateOutputFormat(format) {
104
+ if (!format || format === 'text') {
105
+ return 'text';
106
+ }
107
+ if (format === 'json') {
108
+ return 'json';
109
+ }
110
+ if (format === 'yaml') {
111
+ return 'yaml';
112
+ }
113
+ console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
114
+ process.exit(1);
115
+ }
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@runloop/rl-cli",
3
+ "version": "0.0.1",
4
+ "description": "Beautiful CLI for Runloop devbox management",
5
+ "type": "module",
6
+ "bin": {
7
+ "rln": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "start": "node dist/cli.js",
13
+ "prepublishOnly": "npm run build",
14
+ "version:patch": "npm version patch",
15
+ "version:minor": "npm version minor",
16
+ "version:major": "npm version major",
17
+ "release": "npm run build && npm publish"
18
+ },
19
+ "keywords": [
20
+ "runloop",
21
+ "cli",
22
+ "devbox",
23
+ "cloud",
24
+ "development"
25
+ ],
26
+ "author": "Runloop",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/runloop/rl-cli-node.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/runloop/rl-cli-node/issues"
34
+ },
35
+ "homepage": "https://github.com/runloop/rl-cli-node#readme",
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@inkjs/ui": "^2.0.0",
49
+ "@runloop/api-client": "^0.55.0",
50
+ "chalk": "^5.3.0",
51
+ "commander": "^12.1.0",
52
+ "conf": "^13.0.1",
53
+ "figures": "^6.1.0",
54
+ "gradient-string": "^2.0.2",
55
+ "ink": "^5.0.1",
56
+ "ink-big-text": "^2.0.0",
57
+ "ink-gradient": "^3.0.0",
58
+ "ink-link": "^4.1.0",
59
+ "ink-spinner": "^5.0.0",
60
+ "ink-text-input": "^6.0.0",
61
+ "react": "^18.3.1",
62
+ "yaml": "^2.8.1"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^22.7.9",
66
+ "@types/react": "^18.3.11",
67
+ "typescript": "^5.6.3"
68
+ }
69
+ }