@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 +59 -13
- package/dist/commands/auth.js +2 -1
- package/dist/commands/blueprint/list.js +18 -7
- package/dist/commands/devbox/create.js +1 -2
- package/dist/commands/devbox/list.js +307 -75
- package/dist/commands/menu.js +70 -0
- package/dist/commands/snapshot/list.js +18 -9
- package/dist/components/ActionsPopup.js +42 -0
- package/dist/components/Banner.js +3 -6
- package/dist/components/Breadcrumb.js +3 -4
- package/dist/components/DevboxActionsMenu.js +481 -0
- package/dist/components/DevboxCreatePage.js +15 -7
- package/dist/components/DevboxDetailPage.js +57 -364
- package/dist/components/MainMenu.js +71 -0
- package/dist/components/StatusBadge.js +15 -12
- package/dist/utils/CommandExecutor.js +12 -0
- package/dist/utils/client.js +17 -0
- package/dist/utils/interactiveCommand.js +14 -0
- package/dist/utils/sshSession.js +25 -0
- package/dist/utils/url.js +39 -0
- package/package.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
|
+
import figures from 'figures';
|
|
5
|
+
import { Banner } from './Banner.js';
|
|
6
|
+
import { Breadcrumb } from './Breadcrumb.js';
|
|
7
|
+
import { VERSION } from '../cli.js';
|
|
8
|
+
export const MainMenu = React.memo(({ onSelect }) => {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
11
|
+
// Calculate terminal height once at mount and memoize
|
|
12
|
+
const terminalHeight = React.useMemo(() => process.stdout.rows || 24, []);
|
|
13
|
+
const menuItems = React.useMemo(() => [
|
|
14
|
+
{
|
|
15
|
+
key: 'devboxes',
|
|
16
|
+
label: 'Devboxes',
|
|
17
|
+
description: 'Manage cloud development environments',
|
|
18
|
+
icon: '◉',
|
|
19
|
+
color: 'cyan',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: 'blueprints',
|
|
23
|
+
label: 'Blueprints',
|
|
24
|
+
description: 'Create and manage devbox templates',
|
|
25
|
+
icon: '▣',
|
|
26
|
+
color: 'magenta',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'snapshots',
|
|
30
|
+
label: 'Snapshots',
|
|
31
|
+
description: 'Save and restore devbox states',
|
|
32
|
+
icon: '◈',
|
|
33
|
+
color: 'green',
|
|
34
|
+
},
|
|
35
|
+
], []);
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
38
|
+
setSelectedIndex(selectedIndex - 1);
|
|
39
|
+
}
|
|
40
|
+
else if (key.downArrow && selectedIndex < menuItems.length - 1) {
|
|
41
|
+
setSelectedIndex(selectedIndex + 1);
|
|
42
|
+
}
|
|
43
|
+
else if (key.return) {
|
|
44
|
+
onSelect(menuItems[selectedIndex].key);
|
|
45
|
+
}
|
|
46
|
+
else if (key.escape) {
|
|
47
|
+
exit();
|
|
48
|
+
}
|
|
49
|
+
else if (input === 'd' || input === '1') {
|
|
50
|
+
onSelect('devboxes');
|
|
51
|
+
}
|
|
52
|
+
else if (input === 'b' || input === '2') {
|
|
53
|
+
onSelect('blueprints');
|
|
54
|
+
}
|
|
55
|
+
else if (input === 's' || input === '3') {
|
|
56
|
+
onSelect('snapshots');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Use compact layout if terminal height is less than 20 lines (memoized)
|
|
60
|
+
const useCompactLayout = React.useMemo(() => terminalHeight < 20, [terminalHeight]);
|
|
61
|
+
if (useCompactLayout) {
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { paddingX: 2, marginBottom: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "RUNLOOP.ai" }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "\u2022 Cloud development environments \u2022 v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
|
|
63
|
+
const isSelected = index === selectedIndex;
|
|
64
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : 'gray', children: isSelected ? figures.pointer : ' ' }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : 'white', bold: isSelected, children: item.label }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "- ", item.description] }), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[", index + 1, "]"] })] }, item.key));
|
|
65
|
+
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) })] }));
|
|
66
|
+
}
|
|
67
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Breadcrumb, { items: [{ label: 'Home', active: true }] }), _jsx(Box, { marginBottom: 1, children: _jsx(Banner, {}) }), _jsx(Box, { flexDirection: "column", paddingX: 2, marginBottom: 1, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginBottom: 1, marginTop: 1, children: [_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(Text, { color: "white", bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
68
|
+
const isSelected = index === selectedIndex;
|
|
69
|
+
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: isSelected ? 'round' : 'single', borderColor: isSelected ? item.color : 'gray', marginBottom: 0, children: [_jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : 'white', bold: isSelected, children: item.label }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "gray", dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
70
|
+
})] }), _jsx(Box, { paddingX: 2, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) }) })] }));
|
|
71
|
+
});
|
|
@@ -7,32 +7,35 @@ export const getStatusDisplay = (status) => {
|
|
|
7
7
|
}
|
|
8
8
|
switch (status) {
|
|
9
9
|
case 'running':
|
|
10
|
-
return { icon: figures.circleFilled, color: 'green', text: 'RUNNING' };
|
|
10
|
+
return { icon: figures.circleFilled, color: 'green', text: 'RUNNING ' };
|
|
11
11
|
case 'provisioning':
|
|
12
|
-
return { icon: figures.
|
|
12
|
+
return { icon: figures.ellipsis, color: 'yellow', text: 'PROVISION ' };
|
|
13
13
|
case 'initializing':
|
|
14
|
-
return { icon: figures.ellipsis, color: 'cyan', text: '
|
|
14
|
+
return { icon: figures.ellipsis, color: 'cyan', text: 'INITIALIZE' };
|
|
15
15
|
case 'suspended':
|
|
16
|
-
return { icon: figures.circleDotted, color: 'yellow', text: 'SUSPENDED' };
|
|
16
|
+
return { icon: figures.circleDotted, color: 'yellow', text: 'SUSPENDED ' };
|
|
17
17
|
case 'failure':
|
|
18
|
-
return { icon: figures.cross, color: 'red', text: 'FAILED' };
|
|
18
|
+
return { icon: figures.cross, color: 'red', text: 'FAILED ' };
|
|
19
19
|
case 'shutdown':
|
|
20
|
-
return { icon: figures.circle, color: 'gray', text: 'SHUTDOWN' };
|
|
20
|
+
return { icon: figures.circle, color: 'gray', text: 'SHUTDOWN ' };
|
|
21
21
|
case 'resuming':
|
|
22
|
-
return { icon: figures.ellipsis, color: 'cyan', text: 'RESUMING' };
|
|
22
|
+
return { icon: figures.ellipsis, color: 'cyan', text: 'RESUMING ' };
|
|
23
23
|
case 'suspending':
|
|
24
24
|
return { icon: figures.ellipsis, color: 'yellow', text: 'SUSPENDING' };
|
|
25
25
|
case 'ready':
|
|
26
|
-
return { icon: figures.tick, color: 'green', text: 'READY' };
|
|
26
|
+
return { icon: figures.tick, color: 'green', text: 'READY ' };
|
|
27
27
|
case 'build_complete':
|
|
28
28
|
case 'building_complete':
|
|
29
|
-
return { icon: figures.tick, color: 'green', text: 'COMPLETE' };
|
|
29
|
+
return { icon: figures.tick, color: 'green', text: 'COMPLETE ' };
|
|
30
30
|
case 'building':
|
|
31
|
-
return { icon: figures.ellipsis, color: 'yellow', text: 'BUILDING' };
|
|
31
|
+
return { icon: figures.ellipsis, color: 'yellow', text: 'BUILDING ' };
|
|
32
32
|
case 'build_failed':
|
|
33
|
-
return { icon: figures.cross, color: 'red', text: 'FAILED' };
|
|
33
|
+
return { icon: figures.cross, color: 'red', text: 'FAILED ' };
|
|
34
34
|
default:
|
|
35
|
-
|
|
35
|
+
// Truncate and pad any unknown status to 10 chars to match column width
|
|
36
|
+
const truncated = status.toUpperCase().slice(0, 10);
|
|
37
|
+
const padded = truncated.padEnd(10, ' ');
|
|
38
|
+
return { icon: figures.questionMarkPrefix, color: 'gray', text: padded };
|
|
36
39
|
}
|
|
37
40
|
};
|
|
38
41
|
export const StatusBadge = ({ status, showText = true }) => {
|
|
@@ -28,9 +28,13 @@ export class CommandExecutor {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
// Interactive mode
|
|
31
|
+
// Enter alternate screen buffer
|
|
32
|
+
process.stdout.write('\x1b[?1049h');
|
|
31
33
|
console.clear();
|
|
32
34
|
const { waitUntilExit } = render(renderUI());
|
|
33
35
|
await waitUntilExit();
|
|
36
|
+
// Exit alternate screen buffer
|
|
37
|
+
process.stdout.write('\x1b[?1049l');
|
|
34
38
|
}
|
|
35
39
|
/**
|
|
36
40
|
* Execute a create/action command with automatic format handling
|
|
@@ -47,9 +51,13 @@ export class CommandExecutor {
|
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
53
|
// Interactive mode
|
|
54
|
+
// Enter alternate screen buffer
|
|
55
|
+
process.stdout.write('\x1b[?1049h');
|
|
50
56
|
console.clear();
|
|
51
57
|
const { waitUntilExit } = render(renderUI());
|
|
52
58
|
await waitUntilExit();
|
|
59
|
+
// Exit alternate screen buffer
|
|
60
|
+
process.stdout.write('\x1b[?1049l');
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
63
|
* Execute a delete command with automatic format handling
|
|
@@ -66,8 +74,12 @@ export class CommandExecutor {
|
|
|
66
74
|
return;
|
|
67
75
|
}
|
|
68
76
|
// Interactive mode
|
|
77
|
+
// Enter alternate screen buffer
|
|
78
|
+
process.stdout.write('\x1b[?1049h');
|
|
69
79
|
const { waitUntilExit } = render(renderUI());
|
|
70
80
|
await waitUntilExit();
|
|
81
|
+
// Exit alternate screen buffer
|
|
82
|
+
process.stdout.write('\x1b[?1049l');
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
73
85
|
* Fetch items from an async iterator with optional filtering and limits
|
package/dist/utils/client.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import Runloop from '@runloop/api-client';
|
|
2
2
|
import { getConfig } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get the base URL based on RUNLOOP_ENV environment variable
|
|
5
|
+
* - dev: https://api.runloop.pro
|
|
6
|
+
* - prod or unset: https://api.runloop.ai (default)
|
|
7
|
+
*/
|
|
8
|
+
function getBaseUrl() {
|
|
9
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
10
|
+
switch (env) {
|
|
11
|
+
case 'dev':
|
|
12
|
+
return 'https://api.runloop.pro';
|
|
13
|
+
case 'prod':
|
|
14
|
+
default:
|
|
15
|
+
return 'https://api.runloop.ai';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
3
18
|
export function getClient() {
|
|
4
19
|
const config = getConfig();
|
|
5
20
|
if (!config.apiKey) {
|
|
6
21
|
throw new Error('API key not configured. Run: rln auth');
|
|
7
22
|
}
|
|
23
|
+
const baseURL = getBaseUrl();
|
|
8
24
|
return new Runloop({
|
|
9
25
|
bearerToken: config.apiKey,
|
|
26
|
+
baseURL,
|
|
10
27
|
timeout: 10000, // 10 seconds instead of default 30 seconds
|
|
11
28
|
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
|
|
12
29
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper for interactive commands that need alternate screen buffer management
|
|
3
|
+
*/
|
|
4
|
+
export async function runInteractiveCommand(command) {
|
|
5
|
+
// Enter alternate screen buffer
|
|
6
|
+
process.stdout.write('\x1b[?1049h');
|
|
7
|
+
try {
|
|
8
|
+
await command();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
// Exit alternate screen buffer
|
|
12
|
+
process.stdout.write('\x1b[?1049l');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
export async function runSSHSession(config) {
|
|
3
|
+
// Reset terminal to fix input visibility issues
|
|
4
|
+
// This ensures the terminal is in a proper state after exiting Ink
|
|
5
|
+
spawnSync('reset', [], { stdio: 'inherit' });
|
|
6
|
+
console.clear();
|
|
7
|
+
console.log(`\nConnecting to devbox ${config.devboxName}...\n`);
|
|
8
|
+
// Spawn SSH in foreground with proper terminal settings
|
|
9
|
+
const result = spawnSync('ssh', [
|
|
10
|
+
'-t', // Force pseudo-terminal allocation for proper input handling
|
|
11
|
+
'-i', config.keyPath,
|
|
12
|
+
'-o', `ProxyCommand=${config.proxyCommand}`,
|
|
13
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
14
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
15
|
+
`${config.sshUser}@${config.url}`
|
|
16
|
+
], {
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
shell: false
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
exitCode: result.status || 0,
|
|
22
|
+
shouldRestart: true,
|
|
23
|
+
returnToDevboxId: config.devboxId
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for generating URLs
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Get the base URL for the Runloop platform based on environment
|
|
6
|
+
* - dev: https://platform.runloop.pro
|
|
7
|
+
* - prod or unset: https://platform.runloop.ai (default)
|
|
8
|
+
*/
|
|
9
|
+
export function getBaseUrl() {
|
|
10
|
+
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
11
|
+
switch (env) {
|
|
12
|
+
case 'dev':
|
|
13
|
+
return 'https://platform.runloop.pro';
|
|
14
|
+
case 'prod':
|
|
15
|
+
default:
|
|
16
|
+
return 'https://platform.runloop.ai';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a devbox URL for the given devbox ID
|
|
21
|
+
*/
|
|
22
|
+
export function getDevboxUrl(devboxId) {
|
|
23
|
+
const baseUrl = getBaseUrl();
|
|
24
|
+
return `${baseUrl}/devboxes/${devboxId}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a blueprint URL for the given blueprint ID
|
|
28
|
+
*/
|
|
29
|
+
export function getBlueprintUrl(blueprintId) {
|
|
30
|
+
const baseUrl = getBaseUrl();
|
|
31
|
+
return `${baseUrl}/blueprints/${blueprintId}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate a settings URL
|
|
35
|
+
*/
|
|
36
|
+
export function getSettingsUrl() {
|
|
37
|
+
const baseUrl = getBaseUrl();
|
|
38
|
+
return `${baseUrl}/settings`;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runloop/rl-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Beautiful CLI for Runloop devbox management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@inkjs/ui": "^2.0.0",
|
|
49
49
|
"@runloop/api-client": "^0.55.0",
|
|
50
|
+
"@runloop/rl-cli": "^0.0.1",
|
|
50
51
|
"chalk": "^5.3.0",
|
|
51
52
|
"commander": "^12.1.0",
|
|
52
53
|
"conf": "^13.0.1",
|