@runloop/rl-cli 0.1.2 → 0.3.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 (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  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 +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
package/README.md CHANGED
@@ -63,6 +63,60 @@ export RUNLOOP_API_KEY=your_api_key_here
63
63
 
64
64
  The CLI will automatically use `RUNLOOP_API_KEY` if set, otherwise it will use the stored configuration.
65
65
 
66
+ ### Theme Configuration
67
+
68
+ The CLI supports both light and dark terminal themes with automatic detection:
69
+
70
+ ```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
81
+ ```
82
+
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
+ **How it works:**
91
+
92
+ - **auto** (default): Uses dark mode by default (theme detection is disabled to prevent terminal flashing)
93
+ - **light**: Optimized for light-themed terminals (uses dark text colors)
94
+ - **dark**: Optimized for dark-themed terminals (uses light text colors)
95
+
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
+
66
120
  ### Devbox Commands
67
121
 
68
122
  ```bash
@@ -199,16 +253,6 @@ npm run dev
199
253
  npm start -- <command>
200
254
  ```
201
255
 
202
- ## Tech Stack
203
-
204
- - [Ink](https://github.com/vadimdemedes/ink) - React for CLIs
205
- - [Ink Gradient](https://github.com/sindresorhus/ink-gradient) - Gradient text
206
- - [Ink Big Text](https://github.com/sindresorhus/ink-big-text) - ASCII art
207
- - [Commander.js](https://github.com/tj/commander.js) - CLI framework
208
- - [@runloop/api-client](https://github.com/runloopai/api-client-ts) - Runloop API client
209
- - TypeScript - Type safety
210
- - [Figures](https://github.com/sindresorhus/figures) - Unicode symbols
211
-
212
256
  ## Publishing
213
257
 
214
258
  To publish a new version to npm:
package/dist/cli.js CHANGED
@@ -6,20 +6,15 @@ import { deleteDevbox } from "./commands/devbox/delete.js";
6
6
  import { execCommand } from "./commands/devbox/exec.js";
7
7
  import { uploadFile } from "./commands/devbox/upload.js";
8
8
  import { getConfig } from "./utils/config.js";
9
- import { readFileSync } from "fs";
10
- import { fileURLToPath } from "url";
11
- import { dirname, join } from "path";
12
- // Get version from package.json
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
- const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8"));
16
- export const VERSION = packageJson.version;
9
+ import { VERSION } from "./version.js";
10
+ import { exitAlternateScreenBuffer } from "./utils/screen.js";
11
+ import { processUtils } from "./utils/processUtils.js";
17
12
  // Global Ctrl+C handler to ensure it always exits
18
- process.on("SIGINT", () => {
13
+ processUtils.on("SIGINT", () => {
19
14
  // Force exit immediately, clearing alternate screen buffer
20
- process.stdout.write("\x1b[?1049l");
21
- process.stdout.write("\n");
22
- process.exit(130); // Standard exit code for SIGINT
15
+ exitAlternateScreenBuffer();
16
+ processUtils.stdout.write("\n");
17
+ processUtils.exit(130); // Standard exit code for SIGINT
23
18
  });
24
19
  const program = new Command();
25
20
  program
@@ -33,34 +28,50 @@ program
33
28
  const { default: auth } = await import("./commands/auth.js");
34
29
  auth();
35
30
  });
36
- program
37
- .command("check-updates")
38
- .description("Check for CLI updates")
31
+ // Config commands
32
+ const config = program
33
+ .command("config")
34
+ .description("Configure CLI settings")
39
35
  .action(async () => {
40
- const { checkForUpdates } = await import("./utils/config.js");
41
- console.log("Checking for updates...");
42
- await checkForUpdates(true);
36
+ const { showThemeConfig } = await import("./commands/config.js");
37
+ showThemeConfig();
38
+ });
39
+ config
40
+ .command("theme [mode]")
41
+ .description("Get or set theme mode (auto|light|dark)")
42
+ .action(async (mode) => {
43
+ const { showThemeConfig, setThemeConfig } = await import("./commands/config.js");
44
+ if (!mode) {
45
+ showThemeConfig();
46
+ }
47
+ else if (mode === "auto" || mode === "light" || mode === "dark") {
48
+ setThemeConfig(mode);
49
+ }
50
+ else {
51
+ console.error(`\n❌ Invalid theme mode: ${mode}\nValid options: auto, light, dark\n`);
52
+ processUtils.exit(1);
53
+ }
43
54
  });
44
55
  // Devbox commands
45
56
  const devbox = program
46
57
  .command("devbox")
47
58
  .description("Manage devboxes")
48
- .alias("d")
49
- .action(async () => {
50
- // Open interactive devbox list when no subcommand provided
51
- const { runInteractiveCommand } = await import("./utils/interactiveCommand.js");
52
- const { listDevboxes } = await import("./commands/devbox/list.js");
53
- await runInteractiveCommand(() => listDevboxes({ output: "interactive" }));
54
- });
59
+ .alias("d");
55
60
  devbox
56
61
  .command("create")
57
62
  .description("Create a new devbox")
58
63
  .option("-n, --name <name>", "Devbox name")
59
- .option("-t, --template <template>", "Template to use")
60
- .option("--blueprint <blueprint>", "Blueprint ID to use")
64
+ .option("-t, --template <template>", "Snapshot ID to use (alias: --snapshot)")
65
+ .option("-s, --snapshot <snapshot>", "Snapshot ID to use")
66
+ .option("--blueprint <blueprint>", "Blueprint name or ID to use")
61
67
  .option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
62
68
  .option("--architecture <arch>", "Architecture (arm64, x86_64)")
63
69
  .option("--entrypoint <command>", "Entrypoint command to run")
70
+ .option("--launch-commands <commands...>", "Initialization commands to run on startup")
71
+ .option("--env-vars <vars...>", "Environment variables (format: KEY=value)")
72
+ .option("--code-mounts <mounts...>", "Code mount configurations (JSON format)")
73
+ .option("--idle-time <seconds>", "Idle time in seconds before idle action")
74
+ .option("--idle-action <action>", "Action on idle (shutdown, suspend)")
64
75
  .option("--available-ports <ports...>", "Available ports")
65
76
  .option("--root", "Run as root")
66
77
  .option("--user <user:uid>", "Run as this user (format: username:uid)")
@@ -69,7 +80,8 @@ devbox
69
80
  devbox
70
81
  .command("list")
71
82
  .description("List all devboxes")
72
- .option("-s, --status <status>", "Filter by status")
83
+ .option("-s, --status <status>", "Filter by status (initializing, running, suspending, suspended, resuming, failure, shutdown)")
84
+ .option("-l, --limit <n>", "Max results", "20")
73
85
  .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
74
86
  .action(async (options) => {
75
87
  await listDevboxes(options);
@@ -83,6 +95,7 @@ devbox
83
95
  devbox
84
96
  .command("exec <id> <command...>")
85
97
  .description("Execute a command in a devbox")
98
+ .option("--shell-name <name>", "Shell name to use (optional)")
86
99
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
87
100
  .action(async (id, command, options) => {
88
101
  await execCommand(id, command, options);
@@ -211,6 +224,16 @@ devbox
211
224
  const { getAsync } = await import("./commands/devbox/getAsync.js");
212
225
  await getAsync(id, { executionId, ...options });
213
226
  });
227
+ devbox
228
+ .command("send-stdin <id> <execution-id>")
229
+ .description("Send stdin to a running async execution")
230
+ .option("--text <text>", "Text content to send to stdin")
231
+ .option("--signal <signal>", "Signal to send (EOF, INTERRUPT)")
232
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
233
+ .action(async (id, executionId, options) => {
234
+ const { sendStdin } = await import("./commands/devbox/sendStdin.js");
235
+ await sendStdin(id, executionId, options);
236
+ });
214
237
  devbox
215
238
  .command("logs <id>")
216
239
  .description("View devbox logs")
@@ -223,13 +246,7 @@ devbox
223
246
  const snapshot = program
224
247
  .command("snapshot")
225
248
  .description("Manage devbox snapshots")
226
- .alias("snap")
227
- .action(async () => {
228
- // Open interactive snapshot list when no subcommand provided
229
- const { runInteractiveCommand } = await import("./utils/interactiveCommand.js");
230
- const { listSnapshots } = await import("./commands/snapshot/list.js");
231
- await runInteractiveCommand(() => listSnapshots({ output: "interactive" }));
232
- });
249
+ .alias("snap");
233
250
  snapshot
234
251
  .command("list")
235
252
  .description("List all snapshots")
@@ -257,6 +274,14 @@ snapshot
257
274
  const { deleteSnapshot } = await import("./commands/snapshot/delete.js");
258
275
  deleteSnapshot(id, options);
259
276
  });
277
+ snapshot
278
+ .command("get <id>")
279
+ .description("Get snapshot details")
280
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
281
+ .action(async (id, options) => {
282
+ const { getSnapshot } = await import("./commands/snapshot/get.js");
283
+ await getSnapshot({ id, ...options });
284
+ });
260
285
  snapshot
261
286
  .command("status <snapshot-id>")
262
287
  .description("Get snapshot operation status")
@@ -269,24 +294,20 @@ snapshot
269
294
  const blueprint = program
270
295
  .command("blueprint")
271
296
  .description("Manage blueprints")
272
- .alias("bp")
273
- .action(async () => {
274
- // Open interactive blueprint list when no subcommand provided
275
- const { runInteractiveCommand } = await import("./utils/interactiveCommand.js");
276
- const { listBlueprints } = await import("./commands/blueprint/list.js");
277
- await runInteractiveCommand(() => listBlueprints({ output: "interactive" }));
278
- });
297
+ .alias("bp");
279
298
  blueprint
280
299
  .command("list")
281
300
  .description("List all blueprints")
301
+ .option("-n, --name <name>", "Filter by blueprint name")
282
302
  .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
283
303
  .action(async (options) => {
284
304
  const { listBlueprints } = await import("./commands/blueprint/list.js");
285
305
  await listBlueprints(options);
286
306
  });
287
307
  blueprint
288
- .command("create <name>")
308
+ .command("create")
289
309
  .description("Create a new blueprint")
310
+ .requiredOption("--name <name>", "Blueprint name (required)")
290
311
  .option("--dockerfile <content>", "Dockerfile contents")
291
312
  .option("--dockerfile-path <path>", "Dockerfile path")
292
313
  .option("--system-setup-commands <commands...>", "System setup commands")
@@ -295,37 +316,22 @@ blueprint
295
316
  .option("--available-ports <ports...>", "Available ports")
296
317
  .option("--root", "Run as root")
297
318
  .option("--user <user:uid>", "Run as this user (format: username:uid)")
298
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
299
- .action(async (name, options) => {
319
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
320
+ .action(async (options) => {
300
321
  const { createBlueprint } = await import("./commands/blueprint/create.js");
301
- await createBlueprint({ name, ...options });
302
- });
303
- blueprint
304
- .command("preview <name>")
305
- .description("Preview blueprint before creation")
306
- .option("--dockerfile <content>", "Dockerfile contents")
307
- .option("--system-setup-commands <commands...>", "System setup commands")
308
- .option("--resources <size>", "Resource size (X_SMALL, SMALL, MEDIUM, LARGE, X_LARGE, XX_LARGE)")
309
- .option("--architecture <arch>", "Architecture (arm64, x86_64)")
310
- .option("--available-ports <ports...>", "Available ports")
311
- .option("--root", "Run as root")
312
- .option("--user <user:uid>", "Run as this user (format: username:uid)")
313
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
314
- .action(async (name, options) => {
315
- const { previewBlueprint } = await import("./commands/blueprint/preview.js");
316
- await previewBlueprint({ name, ...options });
322
+ await createBlueprint(options);
317
323
  });
318
324
  blueprint
319
- .command("get <id>")
320
- .description("Get blueprint details")
321
- .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
325
+ .command("get <name-or-id>")
326
+ .description("Get blueprint details by name or ID (IDs start with bpt_)")
327
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
322
328
  .action(async (id, options) => {
323
329
  const { getBlueprint } = await import("./commands/blueprint/get.js");
324
330
  await getBlueprint({ id, ...options });
325
331
  });
326
332
  blueprint
327
- .command("logs <id>")
328
- .description("Get blueprint build logs")
333
+ .command("logs <name-or-id>")
334
+ .description("Get blueprint build logs by name or ID (IDs start with bpt_)")
329
335
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
330
336
  .action(async (id, options) => {
331
337
  const { getBlueprintLogs } = await import("./commands/blueprint/logs.js");
@@ -437,9 +443,13 @@ program
437
443
  });
438
444
  // Main CLI entry point
439
445
  (async () => {
440
- // Check if API key is configured (except for auth and mcp commands)
446
+ // Initialize theme system early (before any UI rendering)
447
+ const { initializeTheme } = await import("./utils/theme.js");
448
+ await initializeTheme();
449
+ // Check if API key is configured (except for auth, config, and mcp commands)
441
450
  const args = process.argv.slice(2);
442
451
  if (args[0] !== "auth" &&
452
+ args[0] !== "config" &&
443
453
  args[0] !== "mcp" &&
444
454
  args[0] !== "mcp-server" &&
445
455
  args[0] !== "--help" &&
@@ -448,18 +458,15 @@ program
448
458
  const config = getConfig();
449
459
  if (!config.apiKey) {
450
460
  console.error("\n❌ API key not configured. Run: rli auth\n");
451
- process.exit(1);
461
+ processUtils.exit(1);
452
462
  }
453
463
  }
454
- // If no command provided, show main menu (version check handled in UI)
464
+ // If no command provided, show main menu
455
465
  if (args.length === 0) {
456
466
  const { runMainMenu } = await import("./commands/menu.js");
457
467
  runMainMenu();
458
468
  }
459
469
  else {
460
- // Check for updates for non-interactive commands (stderr output)
461
- const { checkForUpdates } = await import("./utils/config.js");
462
- await checkForUpdates();
463
470
  program.parse();
464
471
  }
465
472
  })();
@@ -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) {
@@ -24,6 +25,5 @@ const AuthUI = () => {
24
25
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Banner, {}), _jsx(Header, { title: "Authentication" }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: colors.textDim, children: "Get your key: " }), _jsx(Text, { color: colors.primary, children: getSettingsUrl() })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.primary, children: "API Key: " }), _jsx(TextInput, { value: apiKey, onChange: setApiKeyInput, placeholder: "ak_...", mask: "*" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Enter to save" }) })] }));
25
26
  };
26
27
  export default function auth() {
27
- console.clear();
28
28
  render(_jsx(AuthUI, {}));
29
29
  }
@@ -1,78 +1,12 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
3
- 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";
1
+ /**
2
+ * Create blueprint command
3
+ */
9
4
  import { readFile } from "fs/promises";
10
- const CreateBlueprintUI = ({ name, dockerfile, dockerfilePath, systemSetupCommands, resources, architecture, availablePorts, root, user, }) => {
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 createBlueprint = async () => {
16
- try {
17
- const client = getClient();
18
- // Read dockerfile from file if path is provided
19
- let dockerfileContents = dockerfile;
20
- if (dockerfilePath) {
21
- dockerfileContents = await readFile(dockerfilePath, "utf-8");
22
- }
23
- // Parse user parameters
24
- let userParameters = undefined;
25
- if (user && root) {
26
- throw new Error("Only one of --user or --root can be specified");
27
- }
28
- else if (user) {
29
- const [username, uid] = user.split(":");
30
- if (!username || !uid) {
31
- throw new Error("User must be in format 'username:uid'");
32
- }
33
- userParameters = { username, uid: parseInt(uid) };
34
- }
35
- else if (root) {
36
- userParameters = { username: "root", uid: 0 };
37
- }
38
- const blueprint = await client.blueprints.create({
39
- name,
40
- dockerfile: dockerfileContents,
41
- system_setup_commands: systemSetupCommands,
42
- launch_parameters: {
43
- resource_size_request: resources,
44
- architecture: architecture,
45
- available_ports: availablePorts?.map((port) => parseInt(port, 10)),
46
- user_parameters: userParameters,
47
- },
48
- });
49
- setResult(blueprint);
50
- }
51
- catch (err) {
52
- setError(err);
53
- }
54
- finally {
55
- setLoading(false);
56
- }
57
- };
58
- createBlueprint();
59
- }, [
60
- name,
61
- dockerfile,
62
- dockerfilePath,
63
- systemSetupCommands,
64
- resources,
65
- architecture,
66
- availablePorts,
67
- root,
68
- user,
69
- ]);
70
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Creating blueprint..." }), result && (_jsx(SuccessMessage, { message: "Blueprint created successfully", details: `ID: ${result.id}\nName: ${result.name}\nStatus: ${result.status}` })), error && (_jsx(ErrorMessage, { message: "Failed to create blueprint", error: error }))] }));
71
- };
5
+ import { getClient } from "../../utils/client.js";
6
+ import { output, outputError } from "../../utils/output.js";
72
7
  export async function createBlueprint(options) {
73
- const executor = createExecutor({ output: options.output });
74
- await executor.executeAction(async () => {
75
- const client = executor.getClient();
8
+ try {
9
+ const client = getClient();
76
10
  // Read dockerfile from file if path is provided
77
11
  let dockerfileContents = options.dockerfile;
78
12
  if (options.dockerfilePath) {
@@ -81,28 +15,42 @@ export async function createBlueprint(options) {
81
15
  // Parse user parameters
82
16
  let userParameters = undefined;
83
17
  if (options.user && options.root) {
84
- throw new Error("Only one of --user or --root can be specified");
18
+ outputError("Only one of --user or --root can be specified");
85
19
  }
86
20
  else if (options.user) {
87
21
  const [username, uid] = options.user.split(":");
88
22
  if (!username || !uid) {
89
- throw new Error("User must be in format 'username:uid'");
23
+ outputError("User must be in format 'username:uid'");
90
24
  }
91
25
  userParameters = { username, uid: parseInt(uid) };
92
26
  }
93
27
  else if (options.root) {
94
28
  userParameters = { username: "root", uid: 0 };
95
29
  }
96
- return client.blueprints.create({
30
+ // Build launch parameters
31
+ const launchParameters = {};
32
+ if (options.resources) {
33
+ launchParameters.resource_size_request = options.resources;
34
+ }
35
+ if (options.architecture) {
36
+ launchParameters.architecture = options.architecture;
37
+ }
38
+ if (options.availablePorts) {
39
+ launchParameters.available_ports = options.availablePorts.map((port) => parseInt(port, 10));
40
+ }
41
+ if (userParameters) {
42
+ launchParameters.user_parameters = userParameters;
43
+ }
44
+ const blueprint = await client.blueprints.create({
97
45
  name: options.name,
98
46
  dockerfile: dockerfileContents,
99
47
  system_setup_commands: options.systemSetupCommands,
100
- launch_parameters: {
101
- resource_size_request: options.resources,
102
- architecture: options.architecture,
103
- available_ports: options.availablePorts?.map((port) => parseInt(port, 10)),
104
- user_parameters: userParameters,
105
- },
48
+ launch_parameters: launchParameters,
106
49
  });
107
- }, () => (_jsx(CreateBlueprintUI, { name: options.name, dockerfile: options.dockerfile, dockerfilePath: options.dockerfilePath, systemSetupCommands: options.systemSetupCommands, resources: options.resources, architecture: options.architecture, availablePorts: options.availablePorts, root: options.root, user: options.user })));
50
+ // Default: output JSON
51
+ output(blueprint, { format: options.output, defaultFormat: "json" });
52
+ }
53
+ catch (error) {
54
+ outputError("Failed to create blueprint", error);
55
+ }
108
56
  }
@@ -1,37 +1,32 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Get blueprint details 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 GetBlueprintUI = ({ blueprintId }) => {
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 getBlueprint = async () => {
15
- try {
16
- const client = getClient();
17
- const blueprint = await client.blueprints.retrieve(blueprintId);
18
- setResult(blueprint);
19
- }
20
- catch (err) {
21
- setError(err);
22
- }
23
- finally {
24
- setLoading(false);
25
- }
26
- };
27
- getBlueprint();
28
- }, [blueprintId]);
29
- return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Fetching blueprint details..." }), result && (_jsx(SuccessMessage, { message: "Blueprint details retrieved", details: `ID: ${result.id}\nName: ${result.name}\nStatus: ${result.status}\nCreated: ${new Date(result.createdAt).toLocaleString()}` })), error && (_jsx(ErrorMessage, { message: "Failed to get blueprint", error: error }))] }));
30
- };
5
+ import { output, outputError } from "../../utils/output.js";
31
6
  export async function getBlueprint(options) {
32
- const executor = createExecutor({ output: options.output });
33
- await executor.executeAction(async () => {
34
- const client = executor.getClient();
35
- return client.blueprints.retrieve(options.id);
36
- }, () => _jsx(GetBlueprintUI, { blueprintId: options.id }));
7
+ try {
8
+ const client = getClient();
9
+ let blueprint;
10
+ // Check if it's an ID (starts with bpt_) or a name
11
+ if (options.id.startsWith("bpt_")) {
12
+ // It's an ID, retrieve directly
13
+ blueprint = await client.blueprints.retrieve(options.id);
14
+ }
15
+ else {
16
+ // It's a name, search for it
17
+ const result = await client.blueprints.list({ name: options.id });
18
+ const blueprints = result.blueprints || [];
19
+ if (blueprints.length === 0) {
20
+ outputError(`Blueprint not found: ${options.id}`);
21
+ return;
22
+ }
23
+ // Return the first exact match, or first result if no exact match
24
+ blueprint =
25
+ blueprints.find((b) => b.name === options.id) || blueprints[0];
26
+ }
27
+ output(blueprint, { format: options.output, defaultFormat: "json" });
28
+ }
29
+ catch (error) {
30
+ outputError("Failed to get blueprint", error);
31
+ }
37
32
  }