@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
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
})();
|
package/dist/commands/auth.js
CHANGED
|
@@ -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:
|
|
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 =
|
|
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 =
|
|
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 (
|
|
254
|
-
|
|
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: [' ', "[
|
|
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, {}),
|
|
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);
|