@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.
Files changed (42) hide show
  1. package/README.md +5 -75
  2. package/dist/cli.js +24 -56
  3. package/dist/commands/auth.js +2 -1
  4. package/dist/commands/blueprint/list.js +68 -22
  5. package/dist/commands/blueprint/preview.js +38 -42
  6. package/dist/commands/config.js +3 -2
  7. package/dist/commands/devbox/ssh.js +2 -1
  8. package/dist/commands/devbox/tunnel.js +2 -1
  9. package/dist/commands/mcp-http.js +6 -5
  10. package/dist/commands/mcp-install.js +9 -8
  11. package/dist/commands/mcp.js +5 -4
  12. package/dist/commands/menu.js +2 -1
  13. package/dist/components/ActionsPopup.js +18 -17
  14. package/dist/components/Banner.js +7 -1
  15. package/dist/components/Breadcrumb.js +10 -9
  16. package/dist/components/DevboxActionsMenu.js +18 -180
  17. package/dist/components/InteractiveSpawn.js +24 -14
  18. package/dist/components/LogsViewer.js +169 -0
  19. package/dist/components/MainMenu.js +2 -2
  20. package/dist/components/ResourceListView.js +3 -3
  21. package/dist/components/UpdateNotification.js +56 -0
  22. package/dist/hooks/useCursorPagination.js +3 -3
  23. package/dist/hooks/useExitOnCtrlC.js +2 -1
  24. package/dist/mcp/server-http.js +2 -1
  25. package/dist/mcp/server.js +7 -2
  26. package/dist/router/Router.js +3 -1
  27. package/dist/screens/BlueprintLogsScreen.js +74 -0
  28. package/dist/services/blueprintService.js +18 -22
  29. package/dist/utils/CommandExecutor.js +24 -53
  30. package/dist/utils/client.js +5 -1
  31. package/dist/utils/config.js +2 -1
  32. package/dist/utils/logFormatter.js +47 -1
  33. package/dist/utils/output.js +4 -3
  34. package/dist/utils/process.js +106 -0
  35. package/dist/utils/processUtils.js +135 -0
  36. package/dist/utils/screen.js +40 -2
  37. package/dist/utils/ssh.js +3 -2
  38. package/dist/utils/terminalDetection.js +120 -32
  39. package/dist/utils/theme.js +34 -19
  40. package/dist/utils/versionCheck.js +53 -0
  41. package/dist/version.js +12 -0
  42. 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 using either method:
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 with automatic detection:
47
+ The CLI supports both light and dark terminal themes. Set the theme via environment variable:
69
48
 
70
49
  ```bash
71
- # Interactive theme selector with live preview
72
- rli config theme
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): Uses dark mode by default (theme detection is disabled to prevent terminal flashing)
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 { getConfig } from "./utils/config.js";
9
- import { readFileSync } from "fs";
10
- import { fileURLToPath } from "url";
11
- import { dirname, join } from "path";
12
- // Get version from package.json
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
- const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
16
- export const VERSION = packageJson.version;
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
- process.on("SIGINT", () => {
12
+ processUtils.on("SIGINT", () => {
20
13
  // Force exit immediately, clearing alternate screen buffer
21
14
  exitAlternateScreenBuffer();
22
- process.stdout.write("\n");
23
- process.exit(130); // Standard exit code for SIGINT
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 auth, config, and mcp commands)
417
+ // Check if API key is configured (except for mcp commands)
456
418
  const args = process.argv.slice(2);
457
- if (args[0] !== "auth" &&
458
- args[0] !== "config" &&
459
- args[0] !== "mcp" &&
460
- args[0] !== "mcp-server" &&
461
- args[0] !== "--help" &&
462
- args[0] !== "-h" &&
463
- args.length > 0) {
464
- const config = getConfig();
465
- if (!config.apiKey) {
466
- console.error("\n❌ API key not configured. Run: rli auth\n");
467
- process.exit(1);
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) {
@@ -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(() => process.exit(0), 1000);
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
- const blueprint = selectedBlueprint;
192
- if (!blueprint)
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 (executingOperation) {
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 (!needsInput) {
383
- const messages = {
384
- delete: "Deleting blueprint...",
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: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
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
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
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
- * Preview blueprint command
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 { output, outputError } from "../../utils/output.js";
6
- export async function previewBlueprint(options) {
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'");
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
- 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({
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
- output(preview, { format: options.output, defaultFormat: "json" });
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
  }
@@ -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(() => process.exit(0), 1500);
99
+ setTimeout(() => processUtils.exit(0), 1500);
99
100
  }
100
101
  else if (action === "get" || !action) {
101
- setTimeout(() => process.exit(0), 2000);
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
- process.exit(code || 0);
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
- process.exit(code || 0);
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 = { ...process.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
- process.exit(1);
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
- process.exit(code || 1);
29
+ processUtils.exit(code || 1);
29
30
  }
30
31
  });
31
32
  // Handle Ctrl+C
32
- process.on("SIGINT", () => {
33
+ processUtils.on("SIGINT", () => {
33
34
  console.log("\nShutting down MCP HTTP server...");
34
35
  serverProcess.kill("SIGINT");
35
- process.exit(0);
36
+ processUtils.exit(0);
36
37
  });
37
38
  }