@runloop/rl-cli 0.1.1 → 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.
Files changed (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -52
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +20 -90
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -1,40 +1,136 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from "react";
3
- import { Box, Text } from "ink";
1
+ /**
2
+ * Get blueprint build logs command
3
+ */
4
+ import chalk from "chalk";
4
5
  import { getClient } from "../../utils/client.js";
5
- import { Banner } from "../../components/Banner.js";
6
- import { SpinnerComponent } from "../../components/Spinner.js";
7
- import { ErrorMessage } from "../../components/ErrorMessage.js";
8
- import { createExecutor } from "../../utils/CommandExecutor.js";
9
- import { colors } from "../../utils/theme.js";
10
- const BlueprintLogsUI = ({ blueprintId }) => {
11
- const [loading, setLoading] = React.useState(true);
12
- const [result, setResult] = React.useState(null);
13
- const [error, setError] = React.useState(null);
14
- React.useEffect(() => {
15
- const getLogs = async () => {
16
- try {
17
- const client = getClient();
18
- const logs = await client.blueprints.logs(blueprintId);
19
- setResult(logs);
20
- }
21
- catch (err) {
22
- setError(err);
23
- }
24
- finally {
25
- setLoading(false);
26
- }
27
- };
28
- getLogs();
29
- }, [blueprintId]);
30
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && (_jsx(SpinnerComponent, { message: "Fetching blueprint build logs..." })), result && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.primary, children: "Blueprint Build Logs:" }), result.logs && result.logs.length > 0 ? (result.logs.map((log, index) => (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: colors.textDim, children: log.timestampMs
31
- ? new Date(log.timestampMs).toISOString()
32
- : "" }), _jsxs(Text, { color: colors.textDim, children: [" [", log.level, "]"] }), _jsxs(Text, { children: [" ", log.message] })] }, index)))) : (_jsx(Text, { color: colors.textDim, children: "No logs available" }))] })), error && (_jsx(ErrorMessage, { message: "Failed to get blueprint logs", error: error }))] }));
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
- const executor = createExecutor({ output: options.output });
36
- await executor.executeAction(async () => {
37
- const client = executor.getClient();
38
- return client.blueprints.logs(options.id);
39
- }, () => _jsx(BlueprintLogsUI, { blueprintId: options.id }));
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
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Preview blueprint command
3
+ */
3
4
  import { getClient } from "../../utils/client.js";
4
- import { Banner } from "../../components/Banner.js";
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
- const executor = createExecutor({ output: options.output });
37
- await executor.executeAction(async () => {
38
- const client = executor.getClient();
39
- return client.blueprints.preview({
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
- }, () => (_jsx(PreviewBlueprintUI, { name: options.name, dockerfile: options.dockerfile, systemSetupCommands: options.systemSetupCommands })));
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
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from "react";
3
- import { Box, Text } from "ink";
1
+ /**
2
+ * Create devbox command
3
+ */
4
4
  import { getClient } from "../../utils/client.js";
5
- import { Banner } from "../../components/Banner.js";
6
- import { SpinnerComponent } from "../../components/Spinner.js";
7
- import { SuccessMessage } from "../../components/SuccessMessage.js";
8
- import { ErrorMessage } from "../../components/ErrorMessage.js";
9
- import { createExecutor } from "../../utils/CommandExecutor.js";
10
- import { colors } from "../../utils/theme.js";
11
- const CreateDevboxUI = ({ name, template }) => {
12
- const [loading, setLoading] = React.useState(true);
13
- const [result, setResult] = React.useState(null);
14
- const [error, setError] = React.useState(null);
15
- React.useEffect(() => {
16
- const create = async () => {
17
- try {
18
- const client = getClient();
19
- const devbox = await client.devboxes.create({
20
- name: name || `devbox-${Date.now()}`,
21
- ...(template && { template }),
22
- });
23
- setResult(devbox);
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
- catch (err) {
26
- setError(err);
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
- finally {
29
- setLoading(false);
94
+ else {
95
+ createRequest.blueprint_name = options.blueprint;
30
96
  }
31
- };
32
- create();
33
- }, []);
34
- 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: colors.textDim, children: "Try: " }), _jsxs(Text, { color: colors.primary, children: ["rli devbox exec ", result.id, " ls"] })] })] })), error && (_jsx(ErrorMessage, { message: "Failed to create devbox", error: error }))] }));
35
- };
36
- export async function createDevbox(options) {
37
- const executor = createExecutor(options);
38
- await executor.executeAction(async () => {
39
- const client = executor.getClient();
40
- return client.devboxes.create({
41
- name: options.name || `devbox-${Date.now()}`,
42
- ...(options.template && { template: options.template }),
43
- });
44
- }, () => _jsx(CreateDevboxUI, { name: options.name, template: options.template }));
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
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Delete (shutdown) devbox command
3
+ */
3
4
  import { getClient } from "../../utils/client.js";
4
- import { Header } from "../../components/Header.js";
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
- const executor = createExecutor(options);
33
- await executor.executeDelete(async () => {
34
- const client = executor.getClient();
7
+ try {
8
+ const client = getClient();
35
9
  await client.devboxes.shutdown(id);
36
- }, id, () => _jsx(DeleteDevboxUI, { id: id }));
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
  }