@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
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Benchmark Store - Manages benchmark run and scenario run state
3
+ */
4
+ import { create } from "zustand";
5
+ const MAX_CACHE_SIZE = 10;
6
+ export const useBenchmarkStore = create((set, get) => ({
7
+ // Initial benchmark (definition) state
8
+ benchmarks: [],
9
+ benchmarksLoading: false,
10
+ benchmarksError: null,
11
+ benchmarksTotalCount: 0,
12
+ benchmarksHasMore: false,
13
+ benchmarksCurrentPage: 0,
14
+ selectedBenchmarkIndex: 0,
15
+ selectedBenchmarkIds: new Set(),
16
+ // Initial benchmark run state
17
+ benchmarkRuns: [],
18
+ benchmarkRunsLoading: false,
19
+ benchmarkRunsError: null,
20
+ benchmarkRunsTotalCount: 0,
21
+ benchmarkRunsHasMore: false,
22
+ benchmarkRunsCurrentPage: 0,
23
+ // Initial scenario run state
24
+ scenarioRuns: [],
25
+ scenarioRunsLoading: false,
26
+ scenarioRunsError: null,
27
+ scenarioRunsTotalCount: 0,
28
+ scenarioRunsHasMore: false,
29
+ scenarioRunsCurrentPage: 0,
30
+ // Filters
31
+ benchmarkRunIdFilter: undefined,
32
+ // Selection
33
+ selectedBenchmarkRunIndex: 0,
34
+ selectedScenarioRunIndex: 0,
35
+ // Caches
36
+ benchmarkPageCache: new Map(),
37
+ benchmarkRunPageCache: new Map(),
38
+ scenarioRunPageCache: new Map(),
39
+ // Benchmark (definition) Actions
40
+ setBenchmarks: (benchmarks) => set({ benchmarks }),
41
+ setBenchmarksLoading: (loading) => set({ benchmarksLoading: loading }),
42
+ setBenchmarksError: (error) => set({ benchmarksError: error }),
43
+ setBenchmarksTotalCount: (count) => set({ benchmarksTotalCount: count }),
44
+ setBenchmarksHasMore: (hasMore) => set({ benchmarksHasMore: hasMore }),
45
+ setBenchmarksCurrentPage: (page) => set({ benchmarksCurrentPage: page }),
46
+ setSelectedBenchmarkIndex: (index) => set({ selectedBenchmarkIndex: index }),
47
+ setSelectedBenchmarkIds: (ids) => set({ selectedBenchmarkIds: ids }),
48
+ toggleBenchmarkSelection: (id) => {
49
+ const state = get();
50
+ const next = new Set(state.selectedBenchmarkIds);
51
+ if (next.has(id)) {
52
+ next.delete(id);
53
+ }
54
+ else {
55
+ next.add(id);
56
+ }
57
+ set({ selectedBenchmarkIds: next });
58
+ },
59
+ clearBenchmarkSelection: () => set({ selectedBenchmarkIds: new Set() }),
60
+ // Benchmark Run Actions
61
+ setBenchmarkRuns: (runs) => set({ benchmarkRuns: runs }),
62
+ setBenchmarkRunsLoading: (loading) => set({ benchmarkRunsLoading: loading }),
63
+ setBenchmarkRunsError: (error) => set({ benchmarkRunsError: error }),
64
+ setBenchmarkRunsTotalCount: (count) => set({ benchmarkRunsTotalCount: count }),
65
+ setBenchmarkRunsHasMore: (hasMore) => set({ benchmarkRunsHasMore: hasMore }),
66
+ setBenchmarkRunsCurrentPage: (page) => set({ benchmarkRunsCurrentPage: page }),
67
+ setSelectedBenchmarkRunIndex: (index) => set({ selectedBenchmarkRunIndex: index }),
68
+ // Scenario Run Actions
69
+ setScenarioRuns: (runs) => set({ scenarioRuns: runs }),
70
+ setScenarioRunsLoading: (loading) => set({ scenarioRunsLoading: loading }),
71
+ setScenarioRunsError: (error) => set({ scenarioRunsError: error }),
72
+ setScenarioRunsTotalCount: (count) => set({ scenarioRunsTotalCount: count }),
73
+ setScenarioRunsHasMore: (hasMore) => set({ scenarioRunsHasMore: hasMore }),
74
+ setScenarioRunsCurrentPage: (page) => set({ scenarioRunsCurrentPage: page }),
75
+ setSelectedScenarioRunIndex: (index) => set({ selectedScenarioRunIndex: index }),
76
+ setBenchmarkRunIdFilter: (id) => set({ benchmarkRunIdFilter: id }),
77
+ // Cache management
78
+ cacheBenchmarkPage: (page, data) => {
79
+ const state = get();
80
+ const cache = state.benchmarkPageCache;
81
+ if (cache.size >= MAX_CACHE_SIZE) {
82
+ const oldestKey = cache.keys().next().value;
83
+ if (oldestKey !== undefined) {
84
+ cache.delete(oldestKey);
85
+ }
86
+ }
87
+ const plainData = data.map((d) => JSON.parse(JSON.stringify(d)));
88
+ cache.set(page, plainData);
89
+ set({});
90
+ },
91
+ getCachedBenchmarkPage: (page) => {
92
+ return get().benchmarkPageCache.get(page);
93
+ },
94
+ cacheBenchmarkRunPage: (page, data) => {
95
+ const state = get();
96
+ const cache = state.benchmarkRunPageCache;
97
+ if (cache.size >= MAX_CACHE_SIZE) {
98
+ const oldestKey = cache.keys().next().value;
99
+ if (oldestKey !== undefined) {
100
+ cache.delete(oldestKey);
101
+ }
102
+ }
103
+ const plainData = data.map((d) => JSON.parse(JSON.stringify(d)));
104
+ cache.set(page, plainData);
105
+ set({});
106
+ },
107
+ getCachedBenchmarkRunPage: (page) => {
108
+ return get().benchmarkRunPageCache.get(page);
109
+ },
110
+ cacheScenarioRunPage: (page, data) => {
111
+ const state = get();
112
+ const cache = state.scenarioRunPageCache;
113
+ if (cache.size >= MAX_CACHE_SIZE) {
114
+ const oldestKey = cache.keys().next().value;
115
+ if (oldestKey !== undefined) {
116
+ cache.delete(oldestKey);
117
+ }
118
+ }
119
+ const plainData = data.map((d) => JSON.parse(JSON.stringify(d)));
120
+ cache.set(page, plainData);
121
+ set({});
122
+ },
123
+ getCachedScenarioRunPage: (page) => {
124
+ return get().scenarioRunPageCache.get(page);
125
+ },
126
+ clearCache: () => {
127
+ const state = get();
128
+ state.benchmarkPageCache.clear();
129
+ state.benchmarkRunPageCache.clear();
130
+ state.scenarioRunPageCache.clear();
131
+ set({
132
+ benchmarkPageCache: new Map(),
133
+ benchmarkRunPageCache: new Map(),
134
+ scenarioRunPageCache: new Map(),
135
+ });
136
+ },
137
+ clearAll: () => {
138
+ const state = get();
139
+ state.benchmarkPageCache.clear();
140
+ state.benchmarkRunPageCache.clear();
141
+ state.scenarioRunPageCache.clear();
142
+ set({
143
+ benchmarks: [],
144
+ benchmarksLoading: false,
145
+ benchmarksError: null,
146
+ benchmarksTotalCount: 0,
147
+ benchmarksHasMore: false,
148
+ benchmarksCurrentPage: 0,
149
+ selectedBenchmarkIndex: 0,
150
+ selectedBenchmarkIds: new Set(),
151
+ benchmarkRuns: [],
152
+ benchmarkRunsLoading: false,
153
+ benchmarkRunsError: null,
154
+ benchmarkRunsTotalCount: 0,
155
+ benchmarkRunsHasMore: false,
156
+ benchmarkRunsCurrentPage: 0,
157
+ scenarioRuns: [],
158
+ scenarioRunsLoading: false,
159
+ scenarioRunsError: null,
160
+ scenarioRunsTotalCount: 0,
161
+ scenarioRunsHasMore: false,
162
+ scenarioRunsCurrentPage: 0,
163
+ benchmarkRunIdFilter: undefined,
164
+ selectedBenchmarkRunIndex: 0,
165
+ selectedScenarioRunIndex: 0,
166
+ benchmarkPageCache: new Map(),
167
+ benchmarkRunPageCache: new Map(),
168
+ scenarioRunPageCache: new Map(),
169
+ });
170
+ },
171
+ getSelectedBenchmark: () => {
172
+ const state = get();
173
+ return state.benchmarks[state.selectedBenchmarkIndex];
174
+ },
175
+ getSelectedBenchmarkRun: () => {
176
+ const state = get();
177
+ return state.benchmarkRuns[state.selectedBenchmarkRunIndex];
178
+ },
179
+ getSelectedScenarioRun: () => {
180
+ const state = get();
181
+ return state.scenarioRuns[state.selectedScenarioRunIndex];
182
+ },
183
+ }));
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Beta Feature Store - React context for managing beta feature flags
4
+ *
5
+ * Enable beta features by setting the environment variable:
6
+ * export RL_CLI_BETA=1
7
+ * export RL_CLI_BETA=true
8
+ */
9
+ import React from "react";
10
+ import { isBetaEnabled } from "../utils/config.js";
11
+ const BetaFeatureContext = React.createContext(null);
12
+ export function BetaFeatureProvider({ children }) {
13
+ // Read beta status once on mount (env vars don't change during runtime)
14
+ const betaEnabled = React.useMemo(() => isBetaEnabled(), []);
15
+ const isFeatureEnabled = React.useCallback((feature) => {
16
+ // Currently all beta features are gated by the same flag
17
+ // This can be extended to support per-feature flags in the future
18
+ switch (feature) {
19
+ case "benchmarks":
20
+ return betaEnabled;
21
+ default:
22
+ return false;
23
+ }
24
+ }, [betaEnabled]);
25
+ const value = React.useMemo(() => ({
26
+ isBetaEnabled: betaEnabled,
27
+ isFeatureEnabled,
28
+ }), [betaEnabled, isFeatureEnabled]);
29
+ return (_jsx(BetaFeatureContext.Provider, { value: value, children: children }));
30
+ }
31
+ /**
32
+ * Hook to access beta feature flags
33
+ */
34
+ export function useBetaFeatures() {
35
+ const context = React.useContext(BetaFeatureContext);
36
+ if (!context) {
37
+ throw new Error("useBetaFeatures must be used within BetaFeatureProvider");
38
+ }
39
+ return context;
40
+ }
41
+ /**
42
+ * Hook to check if a specific beta feature is enabled
43
+ */
44
+ export function useBetaFeature(feature) {
45
+ const { isFeatureEnabled } = useBetaFeatures();
46
+ return isFeatureEnabled(feature);
47
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Gateway Config Store - Manages gateway configuration state, pagination, and caching
3
+ */
4
+ import { create } from "zustand";
5
+ const MAX_CACHE_SIZE = 10;
6
+ export const useGatewayConfigStore = create((set, get) => ({
7
+ gatewayConfigs: [],
8
+ loading: false,
9
+ initialLoading: true,
10
+ error: null,
11
+ currentPage: 0,
12
+ pageSize: 10,
13
+ totalCount: 0,
14
+ hasMore: false,
15
+ pageCache: new Map(),
16
+ lastIdCache: new Map(),
17
+ searchQuery: "",
18
+ selectedIndex: 0,
19
+ setGatewayConfigs: (configs) => set({ gatewayConfigs: configs }),
20
+ setLoading: (loading) => set({ loading }),
21
+ setInitialLoading: (loading) => set({ initialLoading: loading }),
22
+ setError: (error) => set({ error }),
23
+ setCurrentPage: (page) => set({ currentPage: page }),
24
+ setPageSize: (size) => set({ pageSize: size }),
25
+ setTotalCount: (count) => set({ totalCount: count }),
26
+ setHasMore: (hasMore) => set({ hasMore }),
27
+ setSearchQuery: (query) => set({ searchQuery: query }),
28
+ setSelectedIndex: (index) => set({ selectedIndex: index }),
29
+ cachePageData: (page, data, lastId) => {
30
+ const state = get();
31
+ const pageCache = state.pageCache;
32
+ const lastIdCache = state.lastIdCache;
33
+ // Aggressive LRU eviction
34
+ if (pageCache.size >= MAX_CACHE_SIZE) {
35
+ const oldestKey = pageCache.keys().next().value;
36
+ if (oldestKey !== undefined) {
37
+ pageCache.delete(oldestKey);
38
+ lastIdCache.delete(oldestKey);
39
+ }
40
+ }
41
+ // Deep copy all fields to avoid SDK references
42
+ const plainData = data.map((d) => {
43
+ return JSON.parse(JSON.stringify(d));
44
+ });
45
+ pageCache.set(page, plainData);
46
+ lastIdCache.set(page, lastId);
47
+ set({});
48
+ },
49
+ getCachedPage: (page) => {
50
+ return get().pageCache.get(page);
51
+ },
52
+ clearCache: () => {
53
+ const state = get();
54
+ state.pageCache.clear();
55
+ state.lastIdCache.clear();
56
+ set({
57
+ pageCache: new Map(),
58
+ lastIdCache: new Map(),
59
+ });
60
+ },
61
+ clearAll: () => {
62
+ const state = get();
63
+ state.pageCache.clear();
64
+ state.lastIdCache.clear();
65
+ set({
66
+ gatewayConfigs: [],
67
+ loading: false,
68
+ initialLoading: true,
69
+ error: null,
70
+ currentPage: 0,
71
+ totalCount: 0,
72
+ hasMore: false,
73
+ pageCache: new Map(),
74
+ lastIdCache: new Map(),
75
+ searchQuery: "",
76
+ selectedIndex: 0,
77
+ });
78
+ },
79
+ getSelectedGatewayConfig: () => {
80
+ const state = get();
81
+ return state.gatewayConfigs[state.selectedIndex];
82
+ },
83
+ }));
@@ -2,6 +2,7 @@
2
2
  * Root Store - Exports all stores for easy importing
3
3
  */
4
4
  export { useNavigation, useNavigationStore, NavigationProvider, } from "./navigationStore.js";
5
+ export { BetaFeatureProvider, useBetaFeatures, useBetaFeature, } from "./betaFeatureStore.js";
5
6
  export { useDevboxStore } from "./devboxStore.js";
6
7
  export { useBlueprintStore } from "./blueprintStore.js";
7
8
  export { useSnapshotStore } from "./snapshotStore.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cross-platform browser-opening utility.
3
+ */
4
+ /**
5
+ * Open a URL in the system's default browser.
6
+ * Works on macOS (open), Windows (start), and Linux (xdg-open).
7
+ */
8
+ export async function openInBrowser(url) {
9
+ const { exec } = await import("child_process");
10
+ const platform = process.platform;
11
+ let openCommand;
12
+ if (platform === "darwin") {
13
+ openCommand = `open "${url}"`;
14
+ }
15
+ else if (platform === "win32") {
16
+ openCommand = `start "${url}"`;
17
+ }
18
+ else {
19
+ openCommand = `xdg-open "${url}"`;
20
+ }
21
+ exec(openCommand);
22
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Cross-platform clipboard utility.
3
+ */
4
+ /**
5
+ * Copy text to the system clipboard.
6
+ * Returns a promise that resolves with a status message.
7
+ */
8
+ export async function copyToClipboard(text) {
9
+ const { spawn } = await import("child_process");
10
+ const platform = process.platform;
11
+ let command;
12
+ let args;
13
+ if (platform === "darwin") {
14
+ command = "pbcopy";
15
+ args = [];
16
+ }
17
+ else if (platform === "win32") {
18
+ command = "clip";
19
+ args = [];
20
+ }
21
+ else {
22
+ command = "xclip";
23
+ args = ["-selection", "clipboard"];
24
+ }
25
+ return new Promise((resolve) => {
26
+ const proc = spawn(command, args);
27
+ proc.stdin.write(text);
28
+ proc.stdin.end();
29
+ proc.on("exit", (code) => {
30
+ if (code === 0) {
31
+ resolve("Copied to clipboard!");
32
+ }
33
+ else {
34
+ resolve("Failed to copy");
35
+ }
36
+ });
37
+ proc.on("error", () => {
38
+ resolve("Copy not supported");
39
+ });
40
+ });
41
+ }
@@ -40,6 +40,8 @@ export function createProgram() {
40
40
  .option("--root", "Run as root")
41
41
  .option("--user <user:uid>", "Run as this user (format: username:uid)")
42
42
  .option("--network-policy <id>", "Network policy ID to apply")
43
+ .option("--tunnel <mode>", "Tunnel authentication mode (open, authenticated)")
44
+ .option("--gateways <gateways...>", "Gateway configurations (format: ENV_PREFIX=gateway_id_or_name,secret_id_or_name)")
43
45
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
44
46
  .action(createDevbox);
45
47
  devbox
@@ -248,6 +250,17 @@ export function createProgram() {
248
250
  const { getSnapshot } = await import("../commands/snapshot/get.js");
249
251
  await getSnapshot({ id, ...options });
250
252
  });
253
+ snapshot
254
+ .command("prune <devbox-id>")
255
+ .description("Delete old snapshots for a devbox, keeping only recent ready ones")
256
+ .option("--dry-run", "Show what would be deleted without actually deleting")
257
+ .option("-y, --yes", "Skip confirmation prompt")
258
+ .option("--keep <n>", "Number of ready snapshots to keep", "1")
259
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
260
+ .action(async (devboxId, options) => {
261
+ const { pruneSnapshots } = await import("../commands/snapshot/prune.js");
262
+ await pruneSnapshots(devboxId, options);
263
+ });
251
264
  snapshot
252
265
  .command("status <snapshot-id>")
253
266
  .description("Get snapshot operation status")
@@ -303,6 +316,15 @@ export function createProgram() {
303
316
  const { getBlueprintLogs } = await import("../commands/blueprint/logs.js");
304
317
  await getBlueprintLogs({ id, ...options });
305
318
  });
319
+ blueprint
320
+ .command("delete <id>")
321
+ .description("Delete a blueprint by ID")
322
+ .alias("rm")
323
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
324
+ .action(async (id, options) => {
325
+ const { deleteBlueprint } = await import("../commands/blueprint/delete.js");
326
+ await deleteBlueprint(id, options);
327
+ });
306
328
  blueprint
307
329
  .command("prune <name>")
308
330
  .description("Delete old blueprint builds, keeping only recent successful ones")
@@ -488,6 +510,64 @@ export function createProgram() {
488
510
  const { deleteSecret } = await import("../commands/secret/delete.js");
489
511
  await deleteSecret(name, options);
490
512
  });
513
+ // Gateway config commands
514
+ const gatewayConfig = program
515
+ .command("gateway-config")
516
+ .description("Manage gateway configurations")
517
+ .alias("gwc");
518
+ gatewayConfig
519
+ .command("list")
520
+ .description("List gateway configurations")
521
+ .option("--name <name>", "Filter by name")
522
+ .option("--limit <n>", "Max results", "20")
523
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
524
+ .action(async (options) => {
525
+ const { listGatewayConfigs } = await import("../commands/gateway-config/list.js");
526
+ await listGatewayConfigs(options);
527
+ });
528
+ gatewayConfig
529
+ .command("create")
530
+ .description("Create a new gateway configuration")
531
+ .requiredOption("--name <name>", "Gateway config name (required)")
532
+ .requiredOption("--endpoint <url>", "Target endpoint URL (required)")
533
+ .requiredOption("--auth-type <type>", "Authentication type: bearer or header (required)")
534
+ .option("--auth-key <key>", "Header key name (required for header auth type)")
535
+ .option("--description <description>", "Description")
536
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
537
+ .action(async (options) => {
538
+ const { createGatewayConfig } = await import("../commands/gateway-config/create.js");
539
+ await createGatewayConfig(options);
540
+ });
541
+ gatewayConfig
542
+ .command("get <id>")
543
+ .description("Get gateway configuration details")
544
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
545
+ .action(async (id, options) => {
546
+ const { getGatewayConfig } = await import("../commands/gateway-config/get.js");
547
+ await getGatewayConfig({ id, ...options });
548
+ });
549
+ gatewayConfig
550
+ .command("update <id>")
551
+ .description("Update a gateway configuration")
552
+ .option("--name <name>", "New name")
553
+ .option("--endpoint <url>", "New endpoint URL")
554
+ .option("--auth-type <type>", "New authentication type: bearer or header")
555
+ .option("--auth-key <key>", "New header key name (required for header auth type)")
556
+ .option("--description <description>", "New description")
557
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
558
+ .action(async (id, options) => {
559
+ const { updateGatewayConfig } = await import("../commands/gateway-config/update.js");
560
+ await updateGatewayConfig({ id, ...options });
561
+ });
562
+ gatewayConfig
563
+ .command("delete <id>")
564
+ .description("Delete a gateway configuration")
565
+ .alias("rm")
566
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
567
+ .action(async (id, options) => {
568
+ const { deleteGatewayConfig } = await import("../commands/gateway-config/delete.js");
569
+ await deleteGatewayConfig(id, options);
570
+ });
491
571
  // MCP server commands
492
572
  const mcp = program
493
573
  .command("mcp")
@@ -72,6 +72,14 @@ export function setDetectedTheme(theme) {
72
72
  export function clearDetectedTheme() {
73
73
  config.delete("detectedTheme");
74
74
  }
75
+ /**
76
+ * Check if beta features are enabled via the RL_CLI_BETA environment variable.
77
+ * Set RL_CLI_BETA=1 or RL_CLI_BETA=true to enable beta features.
78
+ */
79
+ export function isBetaEnabled() {
80
+ const betaValue = process.env.RL_CLI_BETA?.toLowerCase();
81
+ return betaValue === "1" || betaValue === "true";
82
+ }
75
83
  /**
76
84
  * Returns the detailed error message for when the API key is not configured.
77
85
  * This message provides instructions on how to set up the API key.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Shared time formatting utilities using the Temporal API.
3
+ */
4
+ import { Temporal } from "@js-temporal/polyfill";
5
+ /**
6
+ * Get elapsed seconds since the given epoch millisecond timestamp.
7
+ */
8
+ function getElapsedSeconds(timestampMs) {
9
+ const now = Temporal.Now.instant();
10
+ const then = Temporal.Instant.fromEpochMilliseconds(timestampMs);
11
+ return Math.floor(now.since(then).total("second"));
12
+ }
13
+ /**
14
+ * Format a relative timestamp in concise form.
15
+ * Examples: "5s ago", "3m ago", "2h ago", "14d ago", "3mo ago", "1y ago"
16
+ *
17
+ * Used for UI detail components.
18
+ */
19
+ export function formatTimeAgo(timestampMs) {
20
+ const seconds = getElapsedSeconds(timestampMs);
21
+ if (seconds < 60)
22
+ return `${seconds}s ago`;
23
+ const minutes = Math.floor(seconds / 60);
24
+ if (minutes < 60)
25
+ return `${minutes}m ago`;
26
+ const hours = Math.floor(minutes / 60);
27
+ if (hours < 24)
28
+ return `${hours}h ago`;
29
+ const days = Math.floor(hours / 24);
30
+ if (days < 30)
31
+ return `${days}d ago`;
32
+ const months = Math.floor(days / 30);
33
+ if (months < 12)
34
+ return `${months}mo ago`;
35
+ const years = Math.floor(months / 12);
36
+ return `${years}y ago`;
37
+ }
38
+ /**
39
+ * Format a relative timestamp in verbose form.
40
+ * Examples: "5 minutes ago", "3 hours ago", "14 days ago"
41
+ *
42
+ * Used for CLI text output (e.g. prune commands).
43
+ */
44
+ export function formatRelativeTime(timestampMs) {
45
+ if (!timestampMs)
46
+ return "unknown time";
47
+ const seconds = getElapsedSeconds(timestampMs);
48
+ const minutes = Math.floor(seconds / 60);
49
+ const hours = Math.floor(seconds / 3600);
50
+ const days = Math.floor(seconds / 86400);
51
+ if (minutes < 60) {
52
+ return `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
53
+ }
54
+ else if (hours < 24) {
55
+ return `${hours} hour${hours !== 1 ? "s" : ""} ago`;
56
+ }
57
+ else {
58
+ return `${days} day${days !== 1 ? "s" : ""} ago`;
59
+ }
60
+ }
61
+ /**
62
+ * Format a timestamp with HH:MM:SS time and a relative indicator.
63
+ * Examples: "14:30:05 (3m ago)", "01-15 09:00:00 (2d)"
64
+ *
65
+ * Used for resource list views.
66
+ */
67
+ export function formatTimeAgoRich(timestampMs) {
68
+ const instant = Temporal.Instant.fromEpochMilliseconds(timestampMs);
69
+ const zdt = instant.toZonedDateTimeISO(Temporal.Now.timeZoneId());
70
+ const time = `${String(zdt.hour).padStart(2, "0")}:${String(zdt.minute).padStart(2, "0")}:${String(zdt.second).padStart(2, "0")}`;
71
+ const seconds = getElapsedSeconds(timestampMs);
72
+ // Less than 1 minute
73
+ if (seconds < 60)
74
+ return `${time} (${seconds}s ago)`;
75
+ const minutes = Math.floor(seconds / 60);
76
+ // Less than 1 hour
77
+ if (minutes < 60)
78
+ return `${time} (${minutes}m ago)`;
79
+ const hours = Math.floor(minutes / 60);
80
+ // Less than 24 hours
81
+ if (hours < 24)
82
+ return `${time} (${hours}hr ago)`;
83
+ const days = Math.floor(hours / 24);
84
+ const month = String(zdt.month).padStart(2, "0");
85
+ const day = String(zdt.day).padStart(2, "0");
86
+ const dateStr = `${month}-${day}`;
87
+ // 1-7 days - show date + time + relative
88
+ if (days <= 7) {
89
+ return `${dateStr} ${time} (${days}d)`;
90
+ }
91
+ // More than 7 days - just date + time
92
+ return `${dateStr} ${time}`;
93
+ }
94
+ /**
95
+ * Format a timestamp as "locale string (relative ago)".
96
+ * Example: "1/15/2025, 3:30:00 PM (2h ago)"
97
+ *
98
+ * Returns undefined if timestamp is falsy.
99
+ */
100
+ export function formatTimestamp(timestamp) {
101
+ if (!timestamp)
102
+ return undefined;
103
+ const formatted = new Date(timestamp).toLocaleString();
104
+ const ago = formatTimeAgo(timestamp);
105
+ return `${formatted} (${ago})`;
106
+ }
107
+ /**
108
+ * Format a time range as "start → end" or "start (relative ago)" if no end.
109
+ *
110
+ * Returns undefined if createTime is falsy.
111
+ */
112
+ export function formatTimeRange(createTime, endTime) {
113
+ if (!createTime)
114
+ return undefined;
115
+ const start = new Date(createTime).toLocaleString();
116
+ if (endTime) {
117
+ const end = new Date(endTime).toLocaleString();
118
+ return `${start} → ${end}`;
119
+ }
120
+ return `${start} (${formatTimeAgo(createTime)})`;
121
+ }