@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.
package/dist/cli.js CHANGED
@@ -6,11 +6,26 @@ import { deleteDevbox } from './commands/devbox/delete.js';
6
6
  import { execCommand } from './commands/devbox/exec.js';
7
7
  import { uploadFile } from './commands/devbox/upload.js';
8
8
  import { getConfig } from './utils/config.js';
9
+ import { readFileSync } from 'fs';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, join } from 'path';
12
+ // Get version from package.json
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
16
+ export const VERSION = packageJson.version;
17
+ // Global Ctrl+C handler to ensure it always exits
18
+ process.on('SIGINT', () => {
19
+ // Force exit immediately, clearing alternate screen buffer
20
+ process.stdout.write('\x1b[?1049l');
21
+ process.stdout.write('\n');
22
+ process.exit(130); // Standard exit code for SIGINT
23
+ });
9
24
  const program = new Command();
10
25
  program
11
26
  .name('rln')
12
27
  .description('Beautiful CLI for Runloop devbox management')
13
- .version('1.0.0');
28
+ .version(VERSION);
14
29
  program
15
30
  .command('auth')
16
31
  .description('Configure API authentication')
@@ -35,7 +50,16 @@ devbox
35
50
  .description('List all devboxes')
36
51
  .option('-s, --status <status>', 'Filter by status')
37
52
  .option('-o, --output [format]', 'Output format: text|json|yaml (default: interactive)')
38
- .action(listDevboxes);
53
+ .action(async (options) => {
54
+ // Only use alternate screen for interactive mode
55
+ if (!options.output) {
56
+ const { runInteractiveCommand } = await import('./utils/interactiveCommand.js');
57
+ await runInteractiveCommand(() => listDevboxes(options));
58
+ }
59
+ else {
60
+ await listDevboxes(options);
61
+ }
62
+ });
39
63
  devbox
40
64
  .command('delete <id>')
41
65
  .description('Shutdown a devbox')
@@ -65,7 +89,13 @@ snapshot
65
89
  .option('-o, --output [format]', 'Output format: text|json|yaml (default: interactive)')
66
90
  .action(async (options) => {
67
91
  const { listSnapshots } = await import('./commands/snapshot/list.js');
68
- listSnapshots(options);
92
+ if (!options.output) {
93
+ const { runInteractiveCommand } = await import('./utils/interactiveCommand.js');
94
+ await runInteractiveCommand(() => listSnapshots(options));
95
+ }
96
+ else {
97
+ await listSnapshots(options);
98
+ }
69
99
  });
70
100
  snapshot
71
101
  .command('create <devbox-id>')
@@ -96,15 +126,31 @@ blueprint
96
126
  .option('-o, --output [format]', 'Output format: text|json|yaml (default: interactive)')
97
127
  .action(async (options) => {
98
128
  const { listBlueprints } = await import('./commands/blueprint/list.js');
99
- listBlueprints(options);
129
+ if (!options.output) {
130
+ const { runInteractiveCommand } = await import('./utils/interactiveCommand.js');
131
+ await runInteractiveCommand(() => listBlueprints(options));
132
+ }
133
+ else {
134
+ await listBlueprints(options);
135
+ }
100
136
  });
101
- // Check if API key is configured (except for auth command)
102
- const args = process.argv.slice(2);
103
- if (args[0] !== 'auth' && args[0] !== '--help' && args[0] !== '-h' && args.length > 0) {
104
- const config = getConfig();
105
- if (!config.apiKey) {
106
- console.error('\n❌ API key not configured. Run: rln auth\n');
107
- process.exit(1);
137
+ // Main CLI entry point
138
+ (async () => {
139
+ // Check if API key is configured (except for auth command)
140
+ const args = process.argv.slice(2);
141
+ if (args[0] !== 'auth' && args[0] !== '--help' && args[0] !== '-h' && args.length > 0) {
142
+ const config = getConfig();
143
+ if (!config.apiKey) {
144
+ console.error('\n❌ API key not configured. Run: rln auth\n');
145
+ process.exit(1);
146
+ }
147
+ }
148
+ // If no command provided, show main menu
149
+ if (args.length === 0) {
150
+ const { runMainMenu } = await import('./commands/menu.js');
151
+ runMainMenu();
152
+ }
153
+ else {
154
+ program.parse();
108
155
  }
109
- }
110
- program.parse();
156
+ })();
@@ -6,6 +6,7 @@ import { setApiKey } from '../utils/config.js';
6
6
  import { Header } from '../components/Header.js';
7
7
  import { Banner } from '../components/Banner.js';
8
8
  import { SuccessMessage } from '../components/SuccessMessage.js';
9
+ import { getSettingsUrl } from '../utils/url.js';
9
10
  const AuthUI = () => {
10
11
  const [apiKey, setApiKeyInput] = React.useState('');
11
12
  const [saved, setSaved] = React.useState(false);
@@ -19,7 +20,7 @@ const AuthUI = () => {
19
20
  if (saved) {
20
21
  return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Header, { title: "Authentication" }), _jsx(SuccessMessage, { message: "API key saved!", details: "Try: rln devbox list" })] }));
21
22
  }
22
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Banner, {}), _jsx(Header, { title: "Authentication" }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "gray", children: "Get your key: " }), _jsx(Text, { color: "cyan", children: "https://runloop.ai/settings" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "API Key: " }), _jsx(TextInput, { value: apiKey, onChange: setApiKeyInput, placeholder: "rl_...", mask: "*" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press Enter to save" }) })] }));
23
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Banner, {}), _jsx(Header, { title: "Authentication" }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "gray", children: "Get your key: " }), _jsx(Text, { color: "cyan", children: getSettingsUrl() })] }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "API Key: " }), _jsx(TextInput, { value: apiKey, onChange: setApiKeyInput, placeholder: "ak_...", mask: "*" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press Enter to save" }) })] }));
23
24
  };
24
25
  export default function auth() {
25
26
  console.clear();
@@ -14,6 +14,7 @@ import { MetadataDisplay } from '../../components/MetadataDisplay.js';
14
14
  import { Table, createTextColumn, createComponentColumn } from '../../components/Table.js';
15
15
  import { OperationsMenu } from '../../components/OperationsMenu.js';
16
16
  import { createExecutor } from '../../utils/CommandExecutor.js';
17
+ import { getBlueprintUrl } from '../../utils/url.js';
17
18
  const PAGE_SIZE = 10;
18
19
  const MAX_FETCH = 100;
19
20
  // Format time ago
@@ -36,9 +37,9 @@ const formatTimeAgo = (timestamp) => {
36
37
  const years = Math.floor(months / 12);
37
38
  return `${years}y ago`;
38
39
  };
39
- const ListBlueprintsUI = () => {
40
+ const ListBlueprintsUI = ({ onBack, onExit }) => {
40
41
  const { stdout } = useStdout();
41
- const { exit } = useApp();
42
+ const { exit: inkExit } = useApp();
42
43
  const [loading, setLoading] = React.useState(true);
43
44
  const [blueprints, setBlueprints] = React.useState([]);
44
45
  const [error, setError] = React.useState(null);
@@ -180,7 +181,7 @@ const ListBlueprintsUI = () => {
180
181
  }
181
182
  else if (input === 'o' && selectedBlueprint) {
182
183
  // Open in browser
183
- const url = `https://platform.runloop.ai/blueprints/${selectedBlueprint.id}`;
184
+ const url = getBlueprintUrl(selectedBlueprint.id);
184
185
  const openBrowser = async () => {
185
186
  const { exec } = await import('child_process');
186
187
  const platform = process.platform;
@@ -232,7 +233,7 @@ const ListBlueprintsUI = () => {
232
233
  }
233
234
  else if (input === 'o' && selectedBlueprint) {
234
235
  // Open in browser
235
- const url = `https://platform.runloop.ai/blueprints/${selectedBlueprint.id}`;
236
+ const url = getBlueprintUrl(selectedBlueprint.id);
236
237
  const openBrowser = async () => {
237
238
  const { exec } = await import('child_process');
238
239
  const platform = process.platform;
@@ -250,8 +251,16 @@ const ListBlueprintsUI = () => {
250
251
  };
251
252
  openBrowser();
252
253
  }
253
- else if (input === 'q') {
254
- process.exit(0);
254
+ else if (key.escape) {
255
+ if (onBack) {
256
+ onBack();
257
+ }
258
+ else if (onExit) {
259
+ onExit();
260
+ }
261
+ else {
262
+ inkExit();
263
+ }
255
264
  }
256
265
  });
257
266
  const totalPages = Math.ceil(blueprints.length / PAGE_SIZE);
@@ -360,8 +369,10 @@ const ListBlueprintsUI = () => {
360
369
  visible: showDescription,
361
370
  }),
362
371
  createTextColumn('created', 'Created', (blueprint) => blueprint.create_time_ms ? formatTimeAgo(blueprint.create_time_ms) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
363
- ] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Operations \u2022 [o] Open in Browser \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[q] Quit"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list blueprints", error: error })] }));
372
+ ] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Operations \u2022 [o] Open in Browser \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[Esc] Back"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list blueprints", error: error })] }));
364
373
  };
374
+ // Export the UI component for use in the main menu
375
+ export { ListBlueprintsUI };
365
376
  export async function listBlueprints(options = {}) {
366
377
  const executor = createExecutor(options);
367
378
  await executor.executeList(async () => {
@@ -2,7 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { getClient } from '../../utils/client.js';
5
- import { Header } from '../../components/Header.js';
6
5
  import { Banner } from '../../components/Banner.js';
7
6
  import { SpinnerComponent } from '../../components/Spinner.js';
8
7
  import { SuccessMessage } from '../../components/SuccessMessage.js';
@@ -31,7 +30,7 @@ const CreateDevboxUI = ({ name, template }) => {
31
30
  };
32
31
  create();
33
32
  }, []);
34
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Header, { title: "Create Devbox" }), loading && _jsx(SpinnerComponent, { message: "Creating..." }), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Devbox created!", details: `ID: ${result.id}\nStatus: ${result.status}` }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Try: " }), _jsxs(Text, { color: "cyan", children: ["rln devbox exec ", result.id, " ls"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create devbox", error: error })] }));
33
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Creating..." }), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Devbox created!", details: `ID: ${result.id}\nStatus: ${result.status}` }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Try: " }), _jsxs(Text, { color: "cyan", children: ["rln devbox exec ", result.id, " ls"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create devbox", error: error })] }));
35
34
  };
36
35
  export async function createDevbox(options) {
37
36
  const executor = createExecutor(options);