@runloop/rl-cli 1.7.1 → 1.9.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 (73) hide show
  1. package/README.md +19 -5
  2. package/dist/cli.js +0 -0
  3. package/dist/commands/blueprint/delete.js +21 -0
  4. package/dist/commands/blueprint/list.js +226 -174
  5. package/dist/commands/blueprint/prune.js +13 -28
  6. package/dist/commands/devbox/create.js +41 -0
  7. package/dist/commands/devbox/list.js +125 -109
  8. package/dist/commands/devbox/tunnel.js +4 -19
  9. package/dist/commands/gateway-config/create.js +44 -0
  10. package/dist/commands/gateway-config/delete.js +21 -0
  11. package/dist/commands/gateway-config/get.js +15 -0
  12. package/dist/commands/gateway-config/list.js +493 -0
  13. package/dist/commands/gateway-config/update.js +60 -0
  14. package/dist/commands/menu.js +2 -1
  15. package/dist/commands/secret/list.js +379 -4
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +108 -0
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +9 -61
  21. package/dist/components/DevboxCreatePage.js +531 -14
  22. package/dist/components/DevboxDetailPage.js +27 -22
  23. package/dist/components/GatewayConfigCreatePage.js +265 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/MainMenu.js +63 -22
  26. package/dist/components/ResourceDetailPage.js +143 -160
  27. package/dist/components/ResourceListView.js +3 -33
  28. package/dist/components/ResourcePicker.js +220 -0
  29. package/dist/components/SecretCreatePage.js +183 -0
  30. package/dist/components/SettingsMenu.js +95 -0
  31. package/dist/components/StateHistory.js +1 -20
  32. package/dist/components/StatusBadge.js +80 -0
  33. package/dist/components/StreamingLogsViewer.js +8 -42
  34. package/dist/components/form/FormTextInput.js +4 -2
  35. package/dist/components/resourceDetailTypes.js +18 -0
  36. package/dist/hooks/useInputHandler.js +103 -0
  37. package/dist/router/Router.js +99 -2
  38. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  39. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  40. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  41. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  42. package/dist/screens/BenchmarkListScreen.js +266 -0
  43. package/dist/screens/BenchmarkMenuScreen.js +29 -0
  44. package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
  45. package/dist/screens/BenchmarkRunListScreen.js +275 -0
  46. package/dist/screens/BlueprintDetailScreen.js +5 -1
  47. package/dist/screens/DevboxCreateScreen.js +2 -2
  48. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  49. package/dist/screens/GatewayConfigListScreen.js +7 -0
  50. package/dist/screens/MenuScreen.js +5 -2
  51. package/dist/screens/ScenarioRunDetailScreen.js +226 -0
  52. package/dist/screens/ScenarioRunListScreen.js +245 -0
  53. package/dist/screens/SecretCreateScreen.js +7 -0
  54. package/dist/screens/SecretDetailScreen.js +198 -0
  55. package/dist/screens/SecretListScreen.js +7 -0
  56. package/dist/screens/SettingsMenuScreen.js +26 -0
  57. package/dist/screens/SnapshotDetailScreen.js +6 -0
  58. package/dist/services/agentService.js +42 -0
  59. package/dist/services/benchmarkJobService.js +122 -0
  60. package/dist/services/benchmarkService.js +120 -0
  61. package/dist/services/gatewayConfigService.js +114 -0
  62. package/dist/services/scenarioService.js +34 -0
  63. package/dist/store/benchmarkJobStore.js +66 -0
  64. package/dist/store/benchmarkStore.js +183 -0
  65. package/dist/store/betaFeatureStore.js +47 -0
  66. package/dist/store/gatewayConfigStore.js +83 -0
  67. package/dist/store/index.js +1 -0
  68. package/dist/utils/browser.js +22 -0
  69. package/dist/utils/clipboard.js +41 -0
  70. package/dist/utils/commands.js +80 -0
  71. package/dist/utils/config.js +8 -0
  72. package/dist/utils/time.js +121 -0
  73. package/package.json +42 -43
@@ -42,6 +42,33 @@ function parseCodeMounts(codeMounts) {
42
42
  }
43
43
  });
44
44
  }
45
+ // Parse gateways from ENV_PREFIX=gateway,secret format
46
+ function parseGateways(gateways) {
47
+ const result = {};
48
+ for (const gateway of gateways) {
49
+ const eqIndex = gateway.indexOf("=");
50
+ if (eqIndex === -1) {
51
+ throw new Error(`Invalid gateway format: ${gateway}. Expected ENV_PREFIX=gateway_id_or_name,secret_id_or_name`);
52
+ }
53
+ const envPrefix = gateway.substring(0, eqIndex);
54
+ const valueStr = gateway.substring(eqIndex + 1);
55
+ // Split by comma to get gateway and secret
56
+ const commaIndex = valueStr.indexOf(",");
57
+ if (commaIndex === -1) {
58
+ throw new Error(`Invalid gateway format: ${gateway}. Expected ENV_PREFIX=gateway_id_or_name,secret_id_or_name`);
59
+ }
60
+ const gatewayIdOrName = valueStr.substring(0, commaIndex);
61
+ const secretIdOrName = valueStr.substring(commaIndex + 1);
62
+ if (!envPrefix || !gatewayIdOrName || !secretIdOrName) {
63
+ throw new Error(`Invalid gateway format: ${gateway}. Expected ENV_PREFIX=gateway_id_or_name,secret_id_or_name`);
64
+ }
65
+ result[envPrefix] = {
66
+ gateway: gatewayIdOrName,
67
+ secret: secretIdOrName,
68
+ };
69
+ }
70
+ return result;
71
+ }
45
72
  export async function createDevbox(options = {}) {
46
73
  try {
47
74
  const client = getClient();
@@ -65,6 +92,10 @@ export async function createDevbox(options = {}) {
65
92
  (!options.idleTime && options.idleAction)) {
66
93
  outputError("Both --idle-time and --idle-action must be specified together");
67
94
  }
95
+ // Validate tunnel option
96
+ if (options.tunnel && !["open", "authenticated"].includes(options.tunnel)) {
97
+ outputError("Invalid tunnel mode. Must be either 'open' or 'authenticated'");
98
+ }
68
99
  // Build launch parameters
69
100
  const launchParameters = {};
70
101
  if (options.resources) {
@@ -128,6 +159,16 @@ export async function createDevbox(options = {}) {
128
159
  if (options.secrets && options.secrets.length > 0) {
129
160
  createRequest.secrets = parseSecrets(options.secrets);
130
161
  }
162
+ // Handle tunnel configuration
163
+ if (options.tunnel) {
164
+ createRequest.tunnel = {
165
+ auth_mode: options.tunnel,
166
+ };
167
+ }
168
+ // Handle gateways
169
+ if (options.gateways && options.gateways.length > 0) {
170
+ createRequest.gateways = parseGateways(options.gateways);
171
+ }
131
172
  if (Object.keys(launchParameters).length > 0) {
132
173
  createRequest.launch_parameters = launchParameters;
133
174
  }
@@ -1,6 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Box, Text, useInput, useApp } from "ink";
3
+ import { Box, Text, useApp } from "ink";
4
4
  import figures from "figures";
5
5
  import { getClient } from "../../utils/client.js";
6
6
  import { SpinnerComponent } from "../../components/Spinner.js";
@@ -21,6 +21,8 @@ import { useViewportHeight } from "../../hooks/useViewportHeight.js";
21
21
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
22
22
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
23
23
  import { useListSearch } from "../../hooks/useListSearch.js";
24
+ import { openInBrowser } from "../../utils/browser.js";
25
+ import { useInputHandler, } from "../../hooks/useInputHandler.js";
24
26
  import { colors } from "../../utils/theme.js";
25
27
  import { useDevboxStore } from "../../store/devboxStore.js";
26
28
  const DEFAULT_PAGE_SIZE = 10;
@@ -319,126 +321,140 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
319
321
  return op.key === "logs" || op.key === "delete";
320
322
  })
321
323
  : allOperations;
322
- useInput((input, key) => {
323
- const pageDevboxes = devboxes.length;
324
- // Skip input handling when in search mode - let TextInput handle it
325
- if (search.searchMode) {
326
- if (key.escape) {
327
- search.cancelSearch();
328
- }
324
+ const closePopup = React.useCallback(() => {
325
+ setShowPopup(false);
326
+ setSelectedOperation(0);
327
+ }, []);
328
+ const handleListEscape = React.useCallback(() => {
329
+ if (search.handleEscape())
329
330
  return;
331
+ if (onBack) {
332
+ onBack();
330
333
  }
331
- // Skip input handling when in details view
332
- if (showDetails) {
333
- return;
334
+ else if (onExit) {
335
+ onExit();
334
336
  }
335
- // Skip input handling when in create view
336
- if (showCreate) {
337
- return;
337
+ else {
338
+ inkExit();
338
339
  }
339
- // Skip input handling when in actions view
340
- if (showActions) {
341
- return;
342
- }
343
- // Handle popup navigation
344
- if (showPopup) {
345
- if (key.escape || input === "q") {
346
- setShowPopup(false);
347
- setSelectedOperation(0);
348
- }
349
- else if (key.upArrow && selectedOperation > 0) {
350
- setSelectedOperation(selectedOperation - 1);
351
- }
352
- else if (key.downArrow && selectedOperation < operations.length - 1) {
353
- setSelectedOperation(selectedOperation + 1);
354
- }
355
- else if (key.return) {
356
- setShowPopup(false);
357
- setShowActions(true);
358
- }
359
- else if (input) {
360
- const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
361
- if (matchedOpIndex !== -1) {
362
- setSelectedOperation(matchedOpIndex);
363
- setShowPopup(false);
364
- setShowActions(true);
365
- }
366
- }
340
+ }, [search, onBack, onExit, inkExit]);
341
+ const handleOpenInBrowser = React.useCallback(() => {
342
+ if (!selectedDevbox)
367
343
  return;
368
- }
369
- // Handle list view
370
- if (key.upArrow && selectedIndex > 0) {
371
- setSelectedIndex(selectedIndex - 1);
372
- }
373
- else if (key.downArrow && selectedIndex < pageDevboxes - 1) {
374
- setSelectedIndex(selectedIndex + 1);
375
- }
376
- else if ((input === "n" || key.rightArrow) &&
377
- !loading &&
378
- !navigating &&
379
- hasMore) {
344
+ openInBrowser(getDevboxUrl(selectedDevbox.id));
345
+ }, [selectedDevbox]);
346
+ const goToNextPage = React.useCallback(() => {
347
+ if (!loading && !navigating && hasMore) {
380
348
  nextPage();
381
349
  setSelectedIndex(0);
382
350
  }
383
- else if ((input === "p" || key.leftArrow) &&
384
- !loading &&
385
- !navigating &&
386
- hasPrev) {
351
+ }, [loading, navigating, hasMore, nextPage]);
352
+ const goToPrevPage = React.useCallback(() => {
353
+ if (!loading && !navigating && hasPrev) {
387
354
  prevPage();
388
355
  setSelectedIndex(0);
389
356
  }
390
- else if (key.return) {
391
- if (onNavigateToDetail && selectedDevbox) {
392
- onNavigateToDetail(selectedDevbox.id);
393
- }
394
- else {
395
- setShowDetails(true);
396
- }
397
- }
398
- else if (input === "a") {
399
- setShowPopup(true);
400
- setSelectedOperation(0);
401
- }
402
- else if (input === "c") {
403
- setShowCreate(true);
404
- }
405
- else if (input === "o" && selectedDevbox) {
406
- const url = getDevboxUrl(selectedDevbox.id);
407
- const openBrowser = async () => {
408
- const { exec } = await import("child_process");
409
- const platform = process.platform;
410
- let openCommand;
411
- if (platform === "darwin") {
412
- openCommand = `open "${url}"`;
413
- }
414
- else if (platform === "win32") {
415
- openCommand = `start "${url}"`;
416
- }
417
- else {
418
- openCommand = `xdg-open "${url}"`;
357
+ }, [loading, navigating, hasPrev, prevPage]);
358
+ const inputModes = React.useMemo(() => [
359
+ // Search mode: only escape to cancel, swallow everything else
360
+ {
361
+ name: "search",
362
+ active: () => search.searchMode,
363
+ bindings: {
364
+ escape: () => search.cancelSearch(),
365
+ },
366
+ captureAll: true,
367
+ },
368
+ // Subview guards: swallow all input when a child view is active
369
+ {
370
+ name: "subviews",
371
+ active: () => showDetails || showCreate || showActions,
372
+ bindings: {},
373
+ captureAll: true,
374
+ },
375
+ // Popup navigation
376
+ {
377
+ name: "popup",
378
+ active: () => showPopup,
379
+ bindings: {
380
+ escape: closePopup,
381
+ q: closePopup,
382
+ up: () => {
383
+ if (selectedOperation > 0)
384
+ setSelectedOperation(selectedOperation - 1);
385
+ },
386
+ down: () => {
387
+ if (selectedOperation < operations.length - 1)
388
+ setSelectedOperation(selectedOperation + 1);
389
+ },
390
+ enter: () => {
391
+ setShowPopup(false);
392
+ setShowActions(true);
393
+ },
394
+ },
395
+ onUnmatched: (input) => {
396
+ const idx = operations.findIndex((op) => op.shortcut === input);
397
+ if (idx !== -1) {
398
+ setSelectedOperation(idx);
399
+ setShowPopup(false);
400
+ setShowActions(true);
419
401
  }
420
- exec(openCommand);
421
- };
422
- openBrowser();
423
- }
424
- else if (input === "/") {
425
- search.enterSearchMode();
426
- }
427
- else if (key.escape) {
428
- if (search.handleEscape()) {
429
- return;
430
- }
431
- if (onBack) {
432
- onBack();
433
- }
434
- else if (onExit) {
435
- onExit();
436
- }
437
- else {
438
- inkExit();
439
- }
440
- }
441
- });
402
+ },
403
+ },
404
+ // List navigation (default mode)
405
+ {
406
+ name: "list",
407
+ active: () => true,
408
+ bindings: {
409
+ up: () => {
410
+ if (selectedIndex > 0)
411
+ setSelectedIndex(selectedIndex - 1);
412
+ },
413
+ down: () => {
414
+ if (selectedIndex < devboxes.length - 1)
415
+ setSelectedIndex(selectedIndex + 1);
416
+ },
417
+ n: goToNextPage,
418
+ right: goToNextPage,
419
+ p: goToPrevPage,
420
+ left: goToPrevPage,
421
+ enter: () => {
422
+ if (onNavigateToDetail && selectedDevbox) {
423
+ onNavigateToDetail(selectedDevbox.id);
424
+ }
425
+ else {
426
+ setShowDetails(true);
427
+ }
428
+ },
429
+ a: () => {
430
+ setShowPopup(true);
431
+ setSelectedOperation(0);
432
+ },
433
+ c: () => setShowCreate(true),
434
+ o: handleOpenInBrowser,
435
+ "/": () => search.enterSearchMode(),
436
+ escape: handleListEscape,
437
+ },
438
+ },
439
+ ], [
440
+ search,
441
+ showDetails,
442
+ showCreate,
443
+ showActions,
444
+ showPopup,
445
+ closePopup,
446
+ selectedOperation,
447
+ operations,
448
+ selectedIndex,
449
+ devboxes.length,
450
+ goToNextPage,
451
+ goToPrevPage,
452
+ onNavigateToDetail,
453
+ selectedDevbox,
454
+ handleOpenInBrowser,
455
+ handleListEscape,
456
+ ]);
457
+ useInputHandler(inputModes);
442
458
  // Create view
443
459
  if (showCreate) {
444
460
  return (_jsx(DevboxCreatePage, { onBack: () => {
@@ -6,6 +6,7 @@ import { getClient } from "../../utils/client.js";
6
6
  import { output, outputError } from "../../utils/output.js";
7
7
  import { processUtils } from "../../utils/processUtils.js";
8
8
  import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
9
+ import { openInBrowser } from "../../utils/browser.js";
9
10
  export async function createTunnel(devboxId, options) {
10
11
  try {
11
12
  // Check if SSH tools are available
@@ -60,25 +61,9 @@ export async function createTunnel(devboxId, options) {
60
61
  // Open browser if --open flag is set
61
62
  if (options.open) {
62
63
  // Small delay to let the tunnel establish
63
- setTimeout(async () => {
64
- const { exec } = await import("child_process");
65
- const platform = process.platform;
66
- let openCommand;
67
- if (platform === "darwin") {
68
- openCommand = `open "${tunnelUrl}"`;
69
- }
70
- else if (platform === "win32") {
71
- openCommand = `start "${tunnelUrl}"`;
72
- }
73
- else {
74
- openCommand = `xdg-open "${tunnelUrl}"`;
75
- }
76
- exec(openCommand, (error) => {
77
- if (error) {
78
- console.log(`\nCould not open browser: ${error.message}`);
79
- }
80
- });
81
- }, 1000);
64
+ setTimeout(() => {
65
+ openInBrowser(tunnelUrl);
66
+ }, 1000); // TODO: Not going to need this soon with tunnels v2
82
67
  }
83
68
  tunnelProcess.on("close", (code) => {
84
69
  console.log("\nTunnel closed.");
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Create gateway config command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function createGatewayConfig(options) {
7
+ try {
8
+ const client = getClient();
9
+ // Validate auth type
10
+ const authType = options.authType.toLowerCase();
11
+ if (authType !== "bearer" && authType !== "header") {
12
+ outputError("Invalid auth type. Must be 'bearer' or 'header'");
13
+ return;
14
+ }
15
+ // Validate auth key is provided for header type
16
+ if (authType === "header" && !options.authKey) {
17
+ outputError("--auth-key is required when auth-type is 'header'");
18
+ return;
19
+ }
20
+ // Build auth mechanism
21
+ const authMechanism = {
22
+ type: authType,
23
+ };
24
+ if (authType === "header" && options.authKey) {
25
+ authMechanism.key = options.authKey;
26
+ }
27
+ const config = await client.gatewayConfigs.create({
28
+ name: options.name,
29
+ endpoint: options.endpoint,
30
+ auth_mechanism: authMechanism,
31
+ description: options.description,
32
+ });
33
+ // Default: just output the ID for easy scripting
34
+ if (!options.output || options.output === "text") {
35
+ console.log(config.id);
36
+ }
37
+ else {
38
+ output(config, { format: options.output, defaultFormat: "json" });
39
+ }
40
+ }
41
+ catch (error) {
42
+ outputError("Failed to create gateway config", error);
43
+ }
44
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Delete gateway config command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function deleteGatewayConfig(id, options = {}) {
7
+ try {
8
+ const client = getClient();
9
+ await client.gatewayConfigs.delete(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: "deleted" }, { format: options.output, defaultFormat: "json" });
16
+ }
17
+ }
18
+ catch (error) {
19
+ outputError("Failed to delete gateway config", error);
20
+ }
21
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Get gateway config command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ export async function getGatewayConfig(options) {
7
+ try {
8
+ const client = getClient();
9
+ const config = await client.gatewayConfigs.retrieve(options.id);
10
+ output(config, { format: options.output, defaultFormat: "json" });
11
+ }
12
+ catch (error) {
13
+ outputError("Failed to get gateway config", error);
14
+ }
15
+ }