@runloop/rl-cli 0.2.0 → 0.4.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 +5 -75
- package/dist/cli.js +24 -56
- package/dist/commands/auth.js +2 -1
- package/dist/commands/blueprint/list.js +68 -22
- package/dist/commands/blueprint/preview.js +38 -42
- package/dist/commands/config.js +3 -2
- package/dist/commands/devbox/ssh.js +2 -1
- package/dist/commands/devbox/tunnel.js +2 -1
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +9 -8
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +2 -1
- package/dist/components/ActionsPopup.js +18 -17
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +10 -9
- package/dist/components/DevboxActionsMenu.js +18 -180
- package/dist/components/InteractiveSpawn.js +24 -14
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +2 -2
- package/dist/components/ResourceListView.js +3 -3
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +3 -3
- package/dist/hooks/useExitOnCtrlC.js +2 -1
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +7 -2
- package/dist/router/Router.js +3 -1
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/services/blueprintService.js +18 -22
- package/dist/utils/CommandExecutor.js +24 -53
- package/dist/utils/client.js +5 -1
- package/dist/utils/config.js +2 -1
- package/dist/utils/logFormatter.js +47 -1
- package/dist/utils/output.js +4 -3
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +40 -2
- package/dist/utils/ssh.js +3 -2
- package/dist/utils/terminalDetection.js +120 -32
- package/dist/utils/theme.js +34 -19
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -6,7 +6,6 @@ A beautiful, interactive CLI for managing Runloop devboxes built with Ink and Ty
|
|
|
6
6
|
|
|
7
7
|
- 🎨 Beautiful terminal UI with colors and gradients
|
|
8
8
|
- ⚡ Fast and responsive with pagination
|
|
9
|
-
- 🔐 Secure API key management
|
|
10
9
|
- 📦 Manage devboxes, snapshots, and blueprints
|
|
11
10
|
- 🚀 Execute commands in devboxes
|
|
12
11
|
- 📤 Upload files to devboxes
|
|
@@ -33,90 +32,31 @@ npm link
|
|
|
33
32
|
|
|
34
33
|
## Setup
|
|
35
34
|
|
|
36
|
-
Configure your API key
|
|
37
|
-
|
|
38
|
-
### Option 1: Environment Variable (Recommended for CI/CD)
|
|
35
|
+
Configure your API key:
|
|
39
36
|
|
|
40
37
|
```bash
|
|
41
38
|
export RUNLOOP_API_KEY=your_api_key_here
|
|
42
39
|
```
|
|
43
40
|
|
|
44
|
-
### Option 2: Interactive Setup
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
rli auth
|
|
48
|
-
```
|
|
49
|
-
|
|
50
41
|
Get your API key from [https://runloop.ai/settings](https://runloop.ai/settings)
|
|
51
42
|
|
|
52
43
|
## Usage
|
|
53
44
|
|
|
54
|
-
### Authentication
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
# Interactive setup (stores API key locally)
|
|
58
|
-
rli auth
|
|
59
|
-
|
|
60
|
-
# Or use environment variable
|
|
61
|
-
export RUNLOOP_API_KEY=your_api_key_here
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
The CLI will automatically use `RUNLOOP_API_KEY` if set, otherwise it will use the stored configuration.
|
|
65
|
-
|
|
66
45
|
### Theme Configuration
|
|
67
46
|
|
|
68
|
-
The CLI supports both light and dark terminal themes
|
|
47
|
+
The CLI supports both light and dark terminal themes. Set the theme via environment variable:
|
|
69
48
|
|
|
70
49
|
```bash
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Or set theme directly
|
|
75
|
-
rli config theme auto # Auto-detect terminal background (default)
|
|
76
|
-
rli config theme light # Force light mode (dark text on light background)
|
|
77
|
-
rli config theme dark # Force dark mode (light text on dark background)
|
|
78
|
-
|
|
79
|
-
# Or use environment variable
|
|
80
|
-
export RUNLOOP_THEME=light
|
|
50
|
+
export RUNLOOP_THEME=light # Force light mode (dark text on light background)
|
|
51
|
+
export RUNLOOP_THEME=dark # Force dark mode (light text on dark background)
|
|
81
52
|
```
|
|
82
53
|
|
|
83
|
-
**Interactive Mode:**
|
|
84
|
-
|
|
85
|
-
- When you run `rli config theme` without arguments, you get an interactive selector
|
|
86
|
-
- Use arrow keys to navigate between auto/light/dark options
|
|
87
|
-
- See live preview of colors as you navigate
|
|
88
|
-
- Press Enter to save, Esc to cancel
|
|
89
|
-
|
|
90
54
|
**How it works:**
|
|
91
55
|
|
|
92
|
-
- **auto** (default):
|
|
56
|
+
- **auto** (default): Detects correct theme by default
|
|
93
57
|
- **light**: Optimized for light-themed terminals (uses dark text colors)
|
|
94
58
|
- **dark**: Optimized for dark-themed terminals (uses light text colors)
|
|
95
59
|
|
|
96
|
-
**Terminal Compatibility:**
|
|
97
|
-
|
|
98
|
-
- Works with all modern terminals (iTerm2, Terminal.app, VS Code integrated terminal, tmux)
|
|
99
|
-
- The CLI defaults to dark mode for the best experience
|
|
100
|
-
- You can manually set light or dark mode based on your terminal theme
|
|
101
|
-
|
|
102
|
-
**Note on Auto-Detection:**
|
|
103
|
-
|
|
104
|
-
- Auto theme detection is **disabled by default** to prevent screen flashing
|
|
105
|
-
- To enable it, set `RUNLOOP_ENABLE_THEME_DETECTION=1`
|
|
106
|
-
- If you use a light terminal, we recommend setting: `rli config theme light`
|
|
107
|
-
- The result is cached, so subsequent runs are instant (no flashing!)
|
|
108
|
-
- If you change your terminal theme, you can re-detect by running:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
rli config theme auto
|
|
112
|
-
```
|
|
113
|
-
- To manually set your theme without detection:
|
|
114
|
-
```bash
|
|
115
|
-
export RUNLOOP_THEME=dark # or light
|
|
116
|
-
# Or disable auto-detection entirely:
|
|
117
|
-
export RUNLOOP_DISABLE_THEME_DETECTION=1
|
|
118
|
-
```
|
|
119
|
-
|
|
120
60
|
### Devbox Commands
|
|
121
61
|
|
|
122
62
|
```bash
|
|
@@ -253,16 +193,6 @@ npm run dev
|
|
|
253
193
|
npm start -- <command>
|
|
254
194
|
```
|
|
255
195
|
|
|
256
|
-
## Tech Stack
|
|
257
|
-
|
|
258
|
-
- [Ink](https://github.com/vadimdemedes/ink) - React for CLIs
|
|
259
|
-
- [Ink Gradient](https://github.com/sindresorhus/ink-gradient) - Gradient text
|
|
260
|
-
- [Ink Big Text](https://github.com/sindresorhus/ink-big-text) - ASCII art
|
|
261
|
-
- [Commander.js](https://github.com/tj/commander.js) - CLI framework
|
|
262
|
-
- [@runloop/api-client](https://github.com/runloopai/api-client-ts) - Runloop API client
|
|
263
|
-
- TypeScript - Type safety
|
|
264
|
-
- [Figures](https://github.com/sindresorhus/figures) - Unicode symbols
|
|
265
|
-
|
|
266
196
|
## Publishing
|
|
267
197
|
|
|
268
198
|
To publish a new version to npm:
|
package/dist/cli.js
CHANGED
|
@@ -5,59 +5,21 @@ import { listDevboxes } from "./commands/devbox/list.js";
|
|
|
5
5
|
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
|
-
import {
|
|
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;
|
|
8
|
+
import { VERSION } from "./version.js";
|
|
17
9
|
import { exitAlternateScreenBuffer } from "./utils/screen.js";
|
|
10
|
+
import { processUtils } from "./utils/processUtils.js";
|
|
18
11
|
// Global Ctrl+C handler to ensure it always exits
|
|
19
|
-
|
|
12
|
+
processUtils.on("SIGINT", () => {
|
|
20
13
|
// Force exit immediately, clearing alternate screen buffer
|
|
21
14
|
exitAlternateScreenBuffer();
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
processUtils.stdout.write("\n");
|
|
16
|
+
processUtils.exit(130); // Standard exit code for SIGINT
|
|
24
17
|
});
|
|
25
18
|
const program = new Command();
|
|
26
19
|
program
|
|
27
20
|
.name("rli")
|
|
28
21
|
.description("Beautiful CLI for Runloop devbox management")
|
|
29
22
|
.version(VERSION);
|
|
30
|
-
program
|
|
31
|
-
.command("auth")
|
|
32
|
-
.description("Configure API authentication")
|
|
33
|
-
.action(async () => {
|
|
34
|
-
const { default: auth } = await import("./commands/auth.js");
|
|
35
|
-
auth();
|
|
36
|
-
});
|
|
37
|
-
// Config commands
|
|
38
|
-
const config = program
|
|
39
|
-
.command("config")
|
|
40
|
-
.description("Configure CLI settings")
|
|
41
|
-
.action(async () => {
|
|
42
|
-
const { showThemeConfig } = await import("./commands/config.js");
|
|
43
|
-
showThemeConfig();
|
|
44
|
-
});
|
|
45
|
-
config
|
|
46
|
-
.command("theme [mode]")
|
|
47
|
-
.description("Get or set theme mode (auto|light|dark)")
|
|
48
|
-
.action(async (mode) => {
|
|
49
|
-
const { showThemeConfig, setThemeConfig } = await import("./commands/config.js");
|
|
50
|
-
if (!mode) {
|
|
51
|
-
showThemeConfig();
|
|
52
|
-
}
|
|
53
|
-
else if (mode === "auto" || mode === "light" || mode === "dark") {
|
|
54
|
-
setThemeConfig(mode);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
console.error(`\n❌ Invalid theme mode: ${mode}\nValid options: auto, light, dark\n`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
23
|
// Devbox commands
|
|
62
24
|
const devbox = program
|
|
63
25
|
.command("devbox")
|
|
@@ -452,20 +414,26 @@ program
|
|
|
452
414
|
// Initialize theme system early (before any UI rendering)
|
|
453
415
|
const { initializeTheme } = await import("./utils/theme.js");
|
|
454
416
|
await initializeTheme();
|
|
455
|
-
// Check if API key is configured (except for
|
|
417
|
+
// Check if API key is configured (except for mcp commands)
|
|
456
418
|
const args = process.argv.slice(2);
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
419
|
+
if (!process.env.RUNLOOP_API_KEY) {
|
|
420
|
+
console.error(`
|
|
421
|
+
❌ API key not configured.
|
|
422
|
+
|
|
423
|
+
To get started:
|
|
424
|
+
1. Go to https://platform.runloop.ai/settings and create an API key
|
|
425
|
+
2. Set the environment variable:
|
|
426
|
+
|
|
427
|
+
export RUNLOOP_API_KEY=your_api_key_here
|
|
428
|
+
|
|
429
|
+
To make it permanent, add this line to your shell config:
|
|
430
|
+
• For zsh: echo 'export RUNLOOP_API_KEY=your_api_key_here' >> ~/.zshrc
|
|
431
|
+
• For bash: echo 'export RUNLOOP_API_KEY=your_api_key_here' >> ~/.bashrc
|
|
432
|
+
|
|
433
|
+
Then restart your terminal or run: source ~/.zshrc (or ~/.bashrc)
|
|
434
|
+
`);
|
|
435
|
+
processUtils.exit(1);
|
|
436
|
+
return; // Ensure execution stops
|
|
469
437
|
}
|
|
470
438
|
// If no command provided, show main menu
|
|
471
439
|
if (args.length === 0) {
|
package/dist/commands/auth.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Banner } from "../components/Banner.js";
|
|
|
8
8
|
import { SuccessMessage } from "../components/SuccessMessage.js";
|
|
9
9
|
import { getSettingsUrl } from "../utils/url.js";
|
|
10
10
|
import { colors } from "../utils/theme.js";
|
|
11
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
11
12
|
const AuthUI = () => {
|
|
12
13
|
const [apiKey, setApiKeyInput] = React.useState("");
|
|
13
14
|
const [saved, setSaved] = React.useState(false);
|
|
@@ -15,7 +16,7 @@ const AuthUI = () => {
|
|
|
15
16
|
if (key.return && apiKey.trim()) {
|
|
16
17
|
setApiKey(apiKey.trim());
|
|
17
18
|
setSaved(true);
|
|
18
|
-
setTimeout(() =>
|
|
19
|
+
setTimeout(() => processUtils.exit(0), 1000);
|
|
19
20
|
}
|
|
20
21
|
});
|
|
21
22
|
if (saved) {
|
|
@@ -20,6 +20,7 @@ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
|
20
20
|
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
21
21
|
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
22
22
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
23
|
+
import { useNavigation } from "../../store/navigationStore.js";
|
|
23
24
|
const DEFAULT_PAGE_SIZE = 10;
|
|
24
25
|
const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
25
26
|
const { exit: inkExit } = useApp();
|
|
@@ -34,6 +35,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
34
35
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
35
36
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
36
37
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
38
|
+
const { navigate } = useNavigation();
|
|
37
39
|
// Calculate overhead for viewport height
|
|
38
40
|
const overhead = 13;
|
|
39
41
|
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
@@ -154,6 +156,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
154
156
|
// Helper function to generate operations based on selected blueprint
|
|
155
157
|
const getOperationsForBlueprint = (blueprint) => {
|
|
156
158
|
const operations = [];
|
|
159
|
+
// View Logs is always available
|
|
160
|
+
operations.push({
|
|
161
|
+
key: "view_logs",
|
|
162
|
+
label: "View Logs",
|
|
163
|
+
color: colors.info,
|
|
164
|
+
icon: figures.info,
|
|
165
|
+
});
|
|
157
166
|
if (blueprint &&
|
|
158
167
|
(blueprint.status === "build_complete" ||
|
|
159
168
|
blueprint.status === "building_complete")) {
|
|
@@ -186,14 +195,33 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
186
195
|
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
187
196
|
const startIndex = currentPage * PAGE_SIZE;
|
|
188
197
|
const endIndex = startIndex + blueprints.length;
|
|
189
|
-
const executeOperation = async () => {
|
|
198
|
+
const executeOperation = async (blueprintOverride, operationOverride) => {
|
|
190
199
|
const client = getClient();
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
// Use override if provided, otherwise use selectedBlueprint from state
|
|
201
|
+
// If neither is available, use selectedBlueprintItem as fallback
|
|
202
|
+
const blueprint = blueprintOverride || selectedBlueprint || selectedBlueprintItem;
|
|
203
|
+
// Use operation override if provided (to avoid state timing issues)
|
|
204
|
+
const operation = operationOverride || executingOperation;
|
|
205
|
+
if (!blueprint) {
|
|
206
|
+
console.error("No blueprint selected for operation");
|
|
193
207
|
return;
|
|
208
|
+
}
|
|
209
|
+
// Ensure selectedBlueprint is set in state if it wasn't already
|
|
210
|
+
if (!selectedBlueprint && blueprint) {
|
|
211
|
+
setSelectedBlueprint(blueprint);
|
|
212
|
+
}
|
|
194
213
|
try {
|
|
195
214
|
setOperationLoading(true);
|
|
196
|
-
switch (
|
|
215
|
+
switch (operation) {
|
|
216
|
+
case "view_logs":
|
|
217
|
+
// Navigate to the logs screen
|
|
218
|
+
setOperationLoading(false);
|
|
219
|
+
setExecutingOperation(null);
|
|
220
|
+
navigate("blueprint-logs", {
|
|
221
|
+
blueprintId: blueprint.id,
|
|
222
|
+
blueprintName: blueprint.name || blueprint.id,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
197
225
|
case "create_devbox":
|
|
198
226
|
setShowCreateDevbox(true);
|
|
199
227
|
setExecutingOperation(null);
|
|
@@ -226,15 +254,18 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
226
254
|
useInput((input, key) => {
|
|
227
255
|
// Handle operation input mode
|
|
228
256
|
if (executingOperation && !operationResult && !operationError) {
|
|
257
|
+
// Allow escape/q to cancel any operation, even during loading
|
|
258
|
+
if (input === "q" || key.escape) {
|
|
259
|
+
setExecutingOperation(null);
|
|
260
|
+
setOperationInput("");
|
|
261
|
+
setOperationLoading(false);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
229
264
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
230
265
|
if (currentOp?.needsInput) {
|
|
231
266
|
if (key.return) {
|
|
232
267
|
executeOperation();
|
|
233
268
|
}
|
|
234
|
-
else if (input === "q" || key.escape) {
|
|
235
|
-
setExecutingOperation(null);
|
|
236
|
-
setOperationInput("");
|
|
237
|
-
}
|
|
238
269
|
}
|
|
239
270
|
return;
|
|
240
271
|
}
|
|
@@ -271,7 +302,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
271
302
|
else {
|
|
272
303
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
273
304
|
setExecutingOperation(operationKey);
|
|
274
|
-
executeOperation();
|
|
305
|
+
executeOperation(selectedBlueprintItem, operationKey);
|
|
275
306
|
}
|
|
276
307
|
}
|
|
277
308
|
else if (key.escape || input === "q") {
|
|
@@ -293,7 +324,16 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
293
324
|
setShowPopup(false);
|
|
294
325
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
295
326
|
setExecutingOperation("delete");
|
|
296
|
-
executeOperation();
|
|
327
|
+
executeOperation(selectedBlueprintItem, "delete");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else if (input === "l") {
|
|
331
|
+
const logsIndex = allOperations.findIndex((op) => op.key === "view_logs");
|
|
332
|
+
if (logsIndex >= 0) {
|
|
333
|
+
setShowPopup(false);
|
|
334
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
335
|
+
setExecutingOperation("view_logs");
|
|
336
|
+
executeOperation(selectedBlueprintItem, "view_logs");
|
|
297
337
|
}
|
|
298
338
|
}
|
|
299
339
|
return;
|
|
@@ -324,6 +364,11 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
324
364
|
setShowPopup(true);
|
|
325
365
|
setSelectedOperation(0);
|
|
326
366
|
}
|
|
367
|
+
else if (input === "l" && selectedBlueprintItem) {
|
|
368
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
369
|
+
setExecutingOperation("view_logs");
|
|
370
|
+
executeOperation(selectedBlueprintItem, "view_logs");
|
|
371
|
+
}
|
|
327
372
|
else if (input === "o" && blueprints[selectedIndex]) {
|
|
328
373
|
const url = getBlueprintUrl(blueprints[selectedIndex].id);
|
|
329
374
|
const openBrowser = async () => {
|
|
@@ -373,27 +418,26 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
373
418
|
const needsInput = currentOp?.needsInput;
|
|
374
419
|
const operationLabel = currentOp?.label || "Operation";
|
|
375
420
|
if (operationLoading) {
|
|
421
|
+
const messages = {
|
|
422
|
+
delete: "Deleting blueprint...",
|
|
423
|
+
view_logs: "Fetching logs...",
|
|
424
|
+
};
|
|
376
425
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
377
426
|
{ label: "Blueprints" },
|
|
378
427
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
379
428
|
{ label: operationLabel, active: true },
|
|
380
|
-
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
429
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [q] or [esc] to cancel" }) })] }));
|
|
381
430
|
}
|
|
382
|
-
if
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
};
|
|
431
|
+
// Only show input screen if operation needs input
|
|
432
|
+
// Operations like view_logs navigate away and don't need this screen
|
|
433
|
+
if (needsInput) {
|
|
386
434
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
387
435
|
{ label: "Blueprints" },
|
|
388
436
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
389
437
|
{ label: operationLabel, active: true },
|
|
390
|
-
] }), _jsx(Header, { title:
|
|
438
|
+
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp?.inputPrompt || "", " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp?.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
391
439
|
}
|
|
392
|
-
|
|
393
|
-
{ label: "Blueprints" },
|
|
394
|
-
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
395
|
-
{ label: operationLabel, active: true },
|
|
396
|
-
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp.inputPrompt, " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp.inputPlaceholder || "" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
440
|
+
// For operations that don't need input (like view_logs), fall through to list view
|
|
397
441
|
}
|
|
398
442
|
// Create devbox screen
|
|
399
443
|
if (showCreateDevbox && selectedBlueprint) {
|
|
@@ -427,7 +471,9 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
427
471
|
? "c"
|
|
428
472
|
: op.key === "delete"
|
|
429
473
|
? "d"
|
|
430
|
-
: ""
|
|
474
|
+
: op.key === "view_logs"
|
|
475
|
+
? "l"
|
|
476
|
+
: "",
|
|
431
477
|
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [o] Browser"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
432
478
|
};
|
|
433
479
|
// Export the UI component for use in the main menu
|
|
@@ -1,49 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
4
3
|
import { getClient } from "../../utils/client.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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);
|
|
18
26
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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({
|
|
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
|
+
};
|
|
35
|
+
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({
|
|
39
40
|
name: options.name,
|
|
40
41
|
dockerfile: options.dockerfile,
|
|
41
42
|
system_setup_commands: options.systemSetupCommands,
|
|
42
|
-
launch_parameters: launchParameters,
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
outputError("Failed to preview blueprint", error);
|
|
48
|
-
}
|
|
44
|
+
}, () => (_jsx(PreviewBlueprintUI, { name: options.name, dockerfile: options.dockerfile, systemSetupCommands: options.systemSetupCommands })));
|
|
49
45
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -6,6 +6,7 @@ import { setThemePreference, getThemePreference, clearDetectedTheme, } from "../
|
|
|
6
6
|
import { Header } from "../components/Header.js";
|
|
7
7
|
import { SuccessMessage } from "../components/SuccessMessage.js";
|
|
8
8
|
import { colors, getCurrentTheme, setThemeMode } from "../utils/theme.js";
|
|
9
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
9
10
|
const themeOptions = [
|
|
10
11
|
{
|
|
11
12
|
value: "auto",
|
|
@@ -95,10 +96,10 @@ const StaticConfigUI = ({ action, value }) => {
|
|
|
95
96
|
clearDetectedTheme();
|
|
96
97
|
}
|
|
97
98
|
setSaved(true);
|
|
98
|
-
setTimeout(() =>
|
|
99
|
+
setTimeout(() => processUtils.exit(0), 1500);
|
|
99
100
|
}
|
|
100
101
|
else if (action === "get" || !action) {
|
|
101
|
-
setTimeout(() =>
|
|
102
|
+
setTimeout(() => processUtils.exit(0), 2000);
|
|
102
103
|
}
|
|
103
104
|
}, [action, value]);
|
|
104
105
|
const currentPreference = getThemePreference();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { getClient } from "../../utils/client.js";
|
|
6
6
|
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
import { processUtils } from "../../utils/processUtils.js";
|
|
7
8
|
import { getSSHKey, waitForReady, generateSSHConfig, checkSSHTools, getProxyCommand, } from "../../utils/ssh.js";
|
|
8
9
|
export async function sshDevbox(devboxId, options = {}) {
|
|
9
10
|
try {
|
|
@@ -59,7 +60,7 @@ export async function sshDevbox(devboxId, options = {}) {
|
|
|
59
60
|
stdio: "inherit",
|
|
60
61
|
});
|
|
61
62
|
sshProcess.on("close", (code) => {
|
|
62
|
-
|
|
63
|
+
processUtils.exit(code || 0);
|
|
63
64
|
});
|
|
64
65
|
sshProcess.on("error", (err) => {
|
|
65
66
|
outputError("SSH connection failed", err);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { spawn } from "child_process";
|
|
5
5
|
import { getClient } from "../../utils/client.js";
|
|
6
6
|
import { output, outputError } from "../../utils/output.js";
|
|
7
|
+
import { processUtils } from "../../utils/processUtils.js";
|
|
7
8
|
import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
|
|
8
9
|
export async function createTunnel(devboxId, options) {
|
|
9
10
|
try {
|
|
@@ -56,7 +57,7 @@ export async function createTunnel(devboxId, options) {
|
|
|
56
57
|
});
|
|
57
58
|
tunnelProcess.on("close", (code) => {
|
|
58
59
|
console.log("\nTunnel closed.");
|
|
59
|
-
|
|
60
|
+
processUtils.exit(code || 0);
|
|
60
61
|
});
|
|
61
62
|
tunnelProcess.on("error", (err) => {
|
|
62
63
|
outputError("Tunnel creation failed", err);
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = dirname(__filename);
|
|
7
8
|
export async function startMcpHttpServer(port) {
|
|
8
9
|
// Get the path to the compiled MCP HTTP server
|
|
9
10
|
const serverPath = join(__dirname, "../mcp/server-http.js");
|
|
10
|
-
const env = { ...
|
|
11
|
+
const env = { ...processUtils.env };
|
|
11
12
|
if (port) {
|
|
12
13
|
env.PORT = port.toString();
|
|
13
14
|
}
|
|
@@ -20,18 +21,18 @@ export async function startMcpHttpServer(port) {
|
|
|
20
21
|
});
|
|
21
22
|
serverProcess.on("error", (error) => {
|
|
22
23
|
console.error("Failed to start MCP HTTP server:", error);
|
|
23
|
-
|
|
24
|
+
processUtils.exit(1);
|
|
24
25
|
});
|
|
25
26
|
serverProcess.on("exit", (code) => {
|
|
26
27
|
if (code !== 0) {
|
|
27
28
|
console.error(`MCP HTTP server exited with code ${code}`);
|
|
28
|
-
|
|
29
|
+
processUtils.exit(code || 1);
|
|
29
30
|
}
|
|
30
31
|
});
|
|
31
32
|
// Handle Ctrl+C
|
|
32
|
-
|
|
33
|
+
processUtils.on("SIGINT", () => {
|
|
33
34
|
console.log("\nShutting down MCP HTTP server...");
|
|
34
35
|
serverProcess.kill("SIGINT");
|
|
35
|
-
|
|
36
|
+
processUtils.exit(0);
|
|
36
37
|
});
|
|
37
38
|
}
|