@runloop/rl-cli 0.1.2 → 0.2.0
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/README.md +54 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +63 -39
- package/dist/components/Breadcrumb.js +10 -48
- package/dist/components/DevboxActionsMenu.js +182 -110
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +94 -0
- package/dist/components/MainMenu.js +36 -32
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +105 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
|
@@ -1,40 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Get blueprint build logs command
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
4
5
|
import { getClient } from "../../utils/client.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
6
|
+
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
function formatLogLevel(level) {
|
|
8
|
+
const normalized = level.toUpperCase();
|
|
9
|
+
switch (normalized) {
|
|
10
|
+
case "ERROR":
|
|
11
|
+
case "ERR":
|
|
12
|
+
return chalk.red.bold("ERROR");
|
|
13
|
+
case "WARN":
|
|
14
|
+
case "WARNING":
|
|
15
|
+
return chalk.yellow.bold("WARN ");
|
|
16
|
+
case "INFO":
|
|
17
|
+
return chalk.blue("INFO ");
|
|
18
|
+
case "DEBUG":
|
|
19
|
+
return chalk.gray("DEBUG");
|
|
20
|
+
default:
|
|
21
|
+
return chalk.gray(normalized.padEnd(5));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function formatTimestamp(timestampMs) {
|
|
25
|
+
const date = new Date(timestampMs);
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
28
|
+
const isThisYear = date.getFullYear() === now.getFullYear();
|
|
29
|
+
const time = date.toLocaleTimeString("en-US", {
|
|
30
|
+
hour12: false,
|
|
31
|
+
hour: "2-digit",
|
|
32
|
+
minute: "2-digit",
|
|
33
|
+
second: "2-digit",
|
|
34
|
+
});
|
|
35
|
+
const ms = date.getMilliseconds().toString().padStart(3, "0");
|
|
36
|
+
if (isToday) {
|
|
37
|
+
// Today: show time with milliseconds for fine granularity
|
|
38
|
+
return chalk.dim(`${time}.${ms}`);
|
|
39
|
+
}
|
|
40
|
+
else if (isThisYear) {
|
|
41
|
+
// This year: show "Jan 5 15:44:03"
|
|
42
|
+
const monthDay = date.toLocaleDateString("en-US", {
|
|
43
|
+
month: "short",
|
|
44
|
+
day: "numeric",
|
|
45
|
+
});
|
|
46
|
+
return chalk.dim(`${monthDay} ${time}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Older: show "Jan 5, 2024 15:44:03"
|
|
50
|
+
const fullDate = date.toLocaleDateString("en-US", {
|
|
51
|
+
year: "numeric",
|
|
52
|
+
month: "short",
|
|
53
|
+
day: "numeric",
|
|
54
|
+
});
|
|
55
|
+
return chalk.dim(`${fullDate} ${time}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function colorizeMessage(message) {
|
|
59
|
+
// Colorize common Docker build patterns
|
|
60
|
+
if (message.startsWith("Step ") || message.startsWith("---> ")) {
|
|
61
|
+
return chalk.cyan.bold(message);
|
|
62
|
+
}
|
|
63
|
+
if (message.startsWith("Successfully")) {
|
|
64
|
+
return chalk.green.bold(message);
|
|
65
|
+
}
|
|
66
|
+
if (message.startsWith("Removing intermediate container")) {
|
|
67
|
+
return chalk.dim(message);
|
|
68
|
+
}
|
|
69
|
+
if (message.toLowerCase().includes("error") ||
|
|
70
|
+
message.toLowerCase().includes("failed")) {
|
|
71
|
+
return chalk.red(message);
|
|
72
|
+
}
|
|
73
|
+
if (message.toLowerCase().includes("warning")) {
|
|
74
|
+
return chalk.yellow(message);
|
|
75
|
+
}
|
|
76
|
+
// Dockerfile instructions
|
|
77
|
+
if (message.startsWith("RUN ") ||
|
|
78
|
+
message.startsWith("COPY ") ||
|
|
79
|
+
message.startsWith("ADD ") ||
|
|
80
|
+
message.startsWith("FROM ") ||
|
|
81
|
+
message.startsWith("WORKDIR ") ||
|
|
82
|
+
message.startsWith("ENV ")) {
|
|
83
|
+
return chalk.yellow(message);
|
|
84
|
+
}
|
|
85
|
+
return message;
|
|
86
|
+
}
|
|
87
|
+
function formatLogEntry(log) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
// Timestamp
|
|
90
|
+
parts.push(formatTimestamp(log.timestamp_ms));
|
|
91
|
+
// Level
|
|
92
|
+
parts.push(formatLogLevel(log.level));
|
|
93
|
+
// Message with colorization
|
|
94
|
+
parts.push(colorizeMessage(log.message));
|
|
95
|
+
return parts.join(" ");
|
|
96
|
+
}
|
|
97
|
+
function formatLogs(response) {
|
|
98
|
+
const logs = response.logs;
|
|
99
|
+
if (!logs || logs.length === 0) {
|
|
100
|
+
console.log(chalk.dim("No build logs available"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const log of logs) {
|
|
104
|
+
console.log(formatLogEntry(log));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
34
107
|
export async function getBlueprintLogs(options) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
108
|
+
try {
|
|
109
|
+
const client = getClient();
|
|
110
|
+
let blueprintId = options.id;
|
|
111
|
+
// Check if it's an ID (starts with bpt_) or a name
|
|
112
|
+
if (!options.id.startsWith("bpt_")) {
|
|
113
|
+
// It's a name, search for it
|
|
114
|
+
const result = await client.blueprints.list({ name: options.id });
|
|
115
|
+
const blueprints = result.blueprints || [];
|
|
116
|
+
if (blueprints.length === 0) {
|
|
117
|
+
outputError(`Blueprint not found: ${options.id}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Use the first exact match, or first result if no exact match
|
|
121
|
+
const blueprint = blueprints.find((b) => b.name === options.id) || blueprints[0];
|
|
122
|
+
blueprintId = blueprint.id;
|
|
123
|
+
}
|
|
124
|
+
const logs = await client.blueprints.logs(blueprintId);
|
|
125
|
+
// Pretty print for text output, JSON for others
|
|
126
|
+
if (!options.output || options.output === "text") {
|
|
127
|
+
formatLogs(logs);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
output(logs, { format: options.output, defaultFormat: "json" });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
outputError("Failed to get blueprint logs", error);
|
|
135
|
+
}
|
|
40
136
|
}
|
|
@@ -1,45 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Preview blueprint command
|
|
3
|
+
*/
|
|
3
4
|
import { getClient } from "../../utils/client.js";
|
|
4
|
-
import {
|
|
5
|
-
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
6
|
-
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
7
|
-
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
8
|
-
import { createExecutor } from "../../utils/CommandExecutor.js";
|
|
9
|
-
const PreviewBlueprintUI = ({ name, dockerfile, systemSetupCommands }) => {
|
|
10
|
-
const [loading, setLoading] = React.useState(true);
|
|
11
|
-
const [result, setResult] = React.useState(null);
|
|
12
|
-
const [error, setError] = React.useState(null);
|
|
13
|
-
React.useEffect(() => {
|
|
14
|
-
const previewBlueprint = async () => {
|
|
15
|
-
try {
|
|
16
|
-
const client = getClient();
|
|
17
|
-
const blueprint = await client.blueprints.preview({
|
|
18
|
-
name,
|
|
19
|
-
dockerfile,
|
|
20
|
-
system_setup_commands: systemSetupCommands,
|
|
21
|
-
});
|
|
22
|
-
setResult(blueprint);
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
setError(err);
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
setLoading(false);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
previewBlueprint();
|
|
32
|
-
}, [name, dockerfile, systemSetupCommands]);
|
|
33
|
-
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Previewing blueprint..." }), result && (_jsx(SuccessMessage, { message: "Blueprint preview generated", details: `Name: ${result.name}\nDockerfile: ${result.dockerfile ? "Present" : "Not provided"}\nSetup Commands: ${result.systemSetupCommands?.length || 0}` })), error && (_jsx(ErrorMessage, { message: "Failed to preview blueprint", error: error }))] }));
|
|
34
|
-
};
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
35
6
|
export async function previewBlueprint(options) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
// Parse user parameters
|
|
10
|
+
let userParameters = undefined;
|
|
11
|
+
if (options.user && options.root) {
|
|
12
|
+
outputError("Only one of --user or --root can be specified");
|
|
13
|
+
}
|
|
14
|
+
else if (options.user) {
|
|
15
|
+
const [username, uid] = options.user.split(":");
|
|
16
|
+
if (!username || !uid) {
|
|
17
|
+
outputError("User must be in format 'username:uid'");
|
|
18
|
+
}
|
|
19
|
+
userParameters = { username, uid: parseInt(uid) };
|
|
20
|
+
}
|
|
21
|
+
else if (options.root) {
|
|
22
|
+
userParameters = { username: "root", uid: 0 };
|
|
23
|
+
}
|
|
24
|
+
// Build launch parameters
|
|
25
|
+
const launchParameters = {};
|
|
26
|
+
if (options.resources) {
|
|
27
|
+
launchParameters.resource_size_request = options.resources;
|
|
28
|
+
}
|
|
29
|
+
if (options.architecture) {
|
|
30
|
+
launchParameters.architecture = options.architecture;
|
|
31
|
+
}
|
|
32
|
+
if (options.availablePorts) {
|
|
33
|
+
launchParameters.available_ports = options.availablePorts.map((port) => parseInt(port, 10));
|
|
34
|
+
}
|
|
35
|
+
if (userParameters) {
|
|
36
|
+
launchParameters.user_parameters = userParameters;
|
|
37
|
+
}
|
|
38
|
+
const preview = await client.blueprints.preview({
|
|
40
39
|
name: options.name,
|
|
41
40
|
dockerfile: options.dockerfile,
|
|
42
41
|
system_setup_commands: options.systemSetupCommands,
|
|
42
|
+
launch_parameters: launchParameters,
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
output(preview, { format: options.output, defaultFormat: "json" });
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
outputError("Failed to preview blueprint", error);
|
|
48
|
+
}
|
|
45
49
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render, Box, Text, useInput, useApp } from "ink";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { setThemePreference, getThemePreference, clearDetectedTheme, } from "../utils/config.js";
|
|
6
|
+
import { Header } from "../components/Header.js";
|
|
7
|
+
import { SuccessMessage } from "../components/SuccessMessage.js";
|
|
8
|
+
import { colors, getCurrentTheme, setThemeMode } from "../utils/theme.js";
|
|
9
|
+
const themeOptions = [
|
|
10
|
+
{
|
|
11
|
+
value: "auto",
|
|
12
|
+
label: "Auto-detect",
|
|
13
|
+
description: "Automatically detect terminal background color",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
value: "dark",
|
|
17
|
+
label: "Dark mode",
|
|
18
|
+
description: "Light text on dark background",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: "light",
|
|
22
|
+
label: "Light mode",
|
|
23
|
+
description: "Dark text on light background",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const InteractiveThemeSelector = ({ initialTheme, }) => {
|
|
27
|
+
const { exit } = useApp();
|
|
28
|
+
const [selectedIndex, setSelectedIndex] = React.useState(() => themeOptions.findIndex((opt) => opt.value === initialTheme));
|
|
29
|
+
const [saved, setSaved] = React.useState(false);
|
|
30
|
+
const [detectedTheme] = React.useState(getCurrentTheme());
|
|
31
|
+
// Update theme preview when selection changes
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
const newTheme = themeOptions[selectedIndex].value;
|
|
34
|
+
let targetTheme;
|
|
35
|
+
if (newTheme === "auto") {
|
|
36
|
+
// For auto mode, show the detected theme
|
|
37
|
+
targetTheme = detectedTheme;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// For explicit light/dark, set directly without detection
|
|
41
|
+
targetTheme = newTheme;
|
|
42
|
+
}
|
|
43
|
+
// Apply theme change for preview
|
|
44
|
+
setThemeMode(targetTheme);
|
|
45
|
+
}, [selectedIndex, detectedTheme]);
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
if (saved) {
|
|
48
|
+
exit();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
52
|
+
setSelectedIndex(selectedIndex - 1);
|
|
53
|
+
}
|
|
54
|
+
else if (key.downArrow && selectedIndex < themeOptions.length - 1) {
|
|
55
|
+
setSelectedIndex(selectedIndex + 1);
|
|
56
|
+
}
|
|
57
|
+
else if (key.return) {
|
|
58
|
+
// Save the selected theme to config
|
|
59
|
+
const selectedTheme = themeOptions[selectedIndex].value;
|
|
60
|
+
setThemePreference(selectedTheme);
|
|
61
|
+
// If setting to 'auto', clear cached detection for re-run
|
|
62
|
+
if (selectedTheme === "auto") {
|
|
63
|
+
clearDetectedTheme();
|
|
64
|
+
}
|
|
65
|
+
setSaved(true);
|
|
66
|
+
setTimeout(() => exit(), 1500);
|
|
67
|
+
}
|
|
68
|
+
else if (key.escape || input === "q") {
|
|
69
|
+
// Restore original theme without re-running detection
|
|
70
|
+
setThemePreference(initialTheme);
|
|
71
|
+
if (initialTheme === "auto") {
|
|
72
|
+
setThemeMode(detectedTheme);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
setThemeMode(initialTheme);
|
|
76
|
+
}
|
|
77
|
+
exit();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
if (saved) {
|
|
81
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Theme Configuration" }), _jsx(SuccessMessage, { message: `Theme set to: ${themeOptions[selectedIndex].label}`, details: "Theme applied immediately!" })] }));
|
|
82
|
+
}
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { title: "Theme Configuration - Interactive" }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Current preference: " }), _jsx(Text, { color: colors.primary, bold: true, children: themeOptions[selectedIndex].label }), themeOptions[selectedIndex].value === "auto" && (_jsxs(Text, { color: colors.textDim, children: [" (detected: ", detectedTheme, ")"] }))] }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.text, bold: true, children: "Select theme mode:" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: themeOptions.map((option, index) => {
|
|
84
|
+
const isSelected = index === selectedIndex;
|
|
85
|
+
return (_jsxs(Box, { marginY: 0, children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsx(Text, { color: isSelected ? colors.primary : colors.text, bold: isSelected, children: option.label }), _jsxs(Text, { color: colors.textDim, children: [" - ", option.description] })] }, option.value));
|
|
86
|
+
}) })] }), _jsxs(Box, { marginTop: 2, flexDirection: "column", children: [_jsxs(Text, { color: colors.text, bold: true, children: [figures.play, " Live Preview:"] }), _jsxs(Box, { marginTop: 1, marginLeft: 2, paddingX: 2, paddingY: 1, borderStyle: "round", borderColor: colors.primary, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.tick, " Primary"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.secondary, bold: true, children: [figures.star, " Secondary"] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.success, children: [figures.tick, " Success"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.warning, children: [figures.warning, " Warning"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.error, children: [figures.cross, " Error"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.text, children: "Normal text" }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.textDim, children: "Dim text" })] })] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Save \u2022 [Esc] Cancel"] }) })] }));
|
|
87
|
+
};
|
|
88
|
+
const StaticConfigUI = ({ action, value }) => {
|
|
89
|
+
const [saved, setSaved] = React.useState(false);
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
if (action === "set" && value) {
|
|
92
|
+
setThemePreference(value);
|
|
93
|
+
// If setting to 'auto', clear the cached detection so it re-runs on next start
|
|
94
|
+
if (value === "auto") {
|
|
95
|
+
clearDetectedTheme();
|
|
96
|
+
}
|
|
97
|
+
setSaved(true);
|
|
98
|
+
setTimeout(() => process.exit(0), 1500);
|
|
99
|
+
}
|
|
100
|
+
else if (action === "get" || !action) {
|
|
101
|
+
setTimeout(() => process.exit(0), 2000);
|
|
102
|
+
}
|
|
103
|
+
}, [action, value]);
|
|
104
|
+
const currentPreference = getThemePreference();
|
|
105
|
+
const activeTheme = getCurrentTheme();
|
|
106
|
+
if (saved) {
|
|
107
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Theme Configuration" }), _jsx(SuccessMessage, { message: `Theme set to: ${value}`, details: "Restart the CLI for changes to take effect" })] }));
|
|
108
|
+
}
|
|
109
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { title: "Theme Configuration" }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Current preference: " }), _jsx(Text, { color: colors.primary, bold: true, children: currentPreference })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Active theme: " }), _jsx(Text, { color: colors.success, bold: true, children: activeTheme })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.text, bold: true, children: "Available options:" }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: ["\u2022 ", _jsx(Text, { color: colors.primary, children: "auto" }), " - Detect terminal background automatically"] }), _jsxs(Text, { color: colors.textDim, children: ["\u2022 ", _jsx(Text, { color: colors.primary, children: "light" }), " - Force light mode (dark text on light background)"] }), _jsxs(Text, { color: colors.textDim, children: ["\u2022 ", _jsx(Text, { color: colors.primary, children: "dark" }), " - Force dark mode (light text on dark background)"] })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: "Usage: rli config theme [auto|light|dark]" }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Environment variable: RUNLOOP_THEME" })] })] }));
|
|
110
|
+
};
|
|
111
|
+
export function showThemeConfig() {
|
|
112
|
+
const currentTheme = getThemePreference();
|
|
113
|
+
render(_jsx(InteractiveThemeSelector, { initialTheme: currentTheme }));
|
|
114
|
+
}
|
|
115
|
+
export function setThemeConfig(theme) {
|
|
116
|
+
render(_jsx(StaticConfigUI, { action: "set", value: theme }));
|
|
117
|
+
}
|
|
@@ -1,45 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Create devbox command
|
|
3
|
+
*/
|
|
4
4
|
import { getClient } from "../../utils/client.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
// Parse environment variables from KEY=value format
|
|
7
|
+
function parseEnvVars(envVars) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const envVar of envVars) {
|
|
10
|
+
const eqIndex = envVar.indexOf("=");
|
|
11
|
+
if (eqIndex === -1) {
|
|
12
|
+
throw new Error(`Invalid environment variable format: ${envVar}. Expected KEY=value`);
|
|
13
|
+
}
|
|
14
|
+
const key = envVar.substring(0, eqIndex);
|
|
15
|
+
const value = envVar.substring(eqIndex + 1);
|
|
16
|
+
result[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
// Parse code mounts from JSON format
|
|
21
|
+
function parseCodeMounts(codeMounts) {
|
|
22
|
+
return codeMounts.map((mount) => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(mount);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Invalid code mount JSON: ${mount}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export async function createDevbox(options = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const client = getClient();
|
|
34
|
+
// Parse user parameters
|
|
35
|
+
let userParameters = undefined;
|
|
36
|
+
if (options.user && options.root) {
|
|
37
|
+
outputError("Only one of --user or --root can be specified");
|
|
38
|
+
}
|
|
39
|
+
else if (options.user) {
|
|
40
|
+
const [username, uid] = options.user.split(":");
|
|
41
|
+
if (!username || !uid) {
|
|
42
|
+
outputError("User must be in format 'username:uid'");
|
|
24
43
|
}
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
userParameters = { username, uid: parseInt(uid) };
|
|
45
|
+
}
|
|
46
|
+
else if (options.root) {
|
|
47
|
+
userParameters = { username: "root", uid: 0 };
|
|
48
|
+
}
|
|
49
|
+
// Validate idle options
|
|
50
|
+
if ((options.idleTime && !options.idleAction) ||
|
|
51
|
+
(!options.idleTime && options.idleAction)) {
|
|
52
|
+
outputError("Both --idle-time and --idle-action must be specified together");
|
|
53
|
+
}
|
|
54
|
+
// Build launch parameters
|
|
55
|
+
const launchParameters = {};
|
|
56
|
+
if (options.resources) {
|
|
57
|
+
launchParameters.resource_size_request = options.resources;
|
|
58
|
+
}
|
|
59
|
+
if (options.architecture) {
|
|
60
|
+
launchParameters.architecture = options.architecture;
|
|
61
|
+
}
|
|
62
|
+
if (options.launchCommands) {
|
|
63
|
+
launchParameters.launch_commands = options.launchCommands;
|
|
64
|
+
}
|
|
65
|
+
if (options.availablePorts) {
|
|
66
|
+
launchParameters.available_ports = options.availablePorts.map((p) => parseInt(p, 10));
|
|
67
|
+
}
|
|
68
|
+
if (userParameters) {
|
|
69
|
+
launchParameters.user_parameters = userParameters;
|
|
70
|
+
}
|
|
71
|
+
if (options.idleTime && options.idleAction) {
|
|
72
|
+
launchParameters.after_idle = {
|
|
73
|
+
idle_time_seconds: parseInt(options.idleTime, 10),
|
|
74
|
+
on_idle: options.idleAction,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Build create request
|
|
78
|
+
const createRequest = {
|
|
79
|
+
name: options.name || `devbox-${Date.now()}`,
|
|
80
|
+
};
|
|
81
|
+
// Handle snapshot (--template and --snapshot are aliases)
|
|
82
|
+
const snapshotId = options.snapshot || options.template;
|
|
83
|
+
if (snapshotId) {
|
|
84
|
+
createRequest.snapshot_id = snapshotId;
|
|
85
|
+
}
|
|
86
|
+
// Handle blueprint - can be either ID or name
|
|
87
|
+
if (options.blueprint) {
|
|
88
|
+
// If it looks like an ID (starts with bp_ or similar pattern), use blueprint_id
|
|
89
|
+
// Otherwise, use blueprint_name
|
|
90
|
+
if (options.blueprint.startsWith("bp_") ||
|
|
91
|
+
options.blueprint.startsWith("bpt_")) {
|
|
92
|
+
createRequest.blueprint_id = options.blueprint;
|
|
27
93
|
}
|
|
28
|
-
|
|
29
|
-
|
|
94
|
+
else {
|
|
95
|
+
createRequest.blueprint_name = options.blueprint;
|
|
30
96
|
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
97
|
+
}
|
|
98
|
+
// Handle entrypoint
|
|
99
|
+
if (options.entrypoint) {
|
|
100
|
+
createRequest.entrypoint = options.entrypoint;
|
|
101
|
+
}
|
|
102
|
+
// Handle environment variables
|
|
103
|
+
if (options.envVars && options.envVars.length > 0) {
|
|
104
|
+
createRequest.environment_variables = parseEnvVars(options.envVars);
|
|
105
|
+
}
|
|
106
|
+
// Handle code mounts
|
|
107
|
+
if (options.codeMounts && options.codeMounts.length > 0) {
|
|
108
|
+
createRequest.code_mounts = parseCodeMounts(options.codeMounts);
|
|
109
|
+
}
|
|
110
|
+
if (Object.keys(launchParameters).length > 0) {
|
|
111
|
+
createRequest.launch_parameters = launchParameters;
|
|
112
|
+
}
|
|
113
|
+
const devbox = await client.devboxes.create(createRequest);
|
|
114
|
+
// Default: just output the ID for easy scripting
|
|
115
|
+
if (!options.output || options.output === "text") {
|
|
116
|
+
console.log(devbox.id);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
output(devbox, { format: options.output, defaultFormat: "json" });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
outputError("Failed to create devbox", error);
|
|
124
|
+
}
|
|
45
125
|
}
|
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Delete (shutdown) devbox command
|
|
3
|
+
*/
|
|
3
4
|
import { getClient } from "../../utils/client.js";
|
|
4
|
-
import {
|
|
5
|
-
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
6
|
-
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
7
|
-
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
8
|
-
import { createExecutor } from "../../utils/CommandExecutor.js";
|
|
9
|
-
const DeleteDevboxUI = ({ id }) => {
|
|
10
|
-
const [loading, setLoading] = React.useState(true);
|
|
11
|
-
const [success, setSuccess] = React.useState(false);
|
|
12
|
-
const [error, setError] = React.useState(null);
|
|
13
|
-
React.useEffect(() => {
|
|
14
|
-
const deleteDevbox = async () => {
|
|
15
|
-
try {
|
|
16
|
-
const client = getClient();
|
|
17
|
-
await client.devboxes.shutdown(id);
|
|
18
|
-
setSuccess(true);
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
setError(err);
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
setLoading(false);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
deleteDevbox();
|
|
28
|
-
}, []);
|
|
29
|
-
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Shutdown Devbox", subtitle: `Shutting down devbox: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Shutting down devbox..." }), success && (_jsx(SuccessMessage, { message: "Devbox shut down successfully!", details: `ID: ${id}` })), error && (_jsx(ErrorMessage, { message: "Failed to shutdown devbox", error: error }))] }));
|
|
30
|
-
};
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
31
6
|
export async function deleteDevbox(id, options = {}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const client = executor.getClient();
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
35
9
|
await client.devboxes.shutdown(id);
|
|
36
|
-
|
|
10
|
+
// Default: just output the ID for easy scripting
|
|
11
|
+
if (!options.output || options.output === "text") {
|
|
12
|
+
console.log(id);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
output({ id, status: "shutdown" }, { format: options.output, defaultFormat: "json" });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
outputError("Failed to shutdown devbox", error);
|
|
20
|
+
}
|
|
37
21
|
}
|