@runloop/rl-cli 1.8.0 → 1.10.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 (66) hide show
  1. package/README.md +21 -7
  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 +142 -110
  8. package/dist/commands/devbox/rsync.js +69 -41
  9. package/dist/commands/devbox/scp.js +180 -39
  10. package/dist/commands/devbox/tunnel.js +4 -19
  11. package/dist/commands/gateway-config/create.js +53 -0
  12. package/dist/commands/gateway-config/delete.js +21 -0
  13. package/dist/commands/gateway-config/get.js +18 -0
  14. package/dist/commands/gateway-config/list.js +493 -0
  15. package/dist/commands/gateway-config/update.js +70 -0
  16. package/dist/commands/snapshot/list.js +11 -2
  17. package/dist/commands/snapshot/prune.js +265 -0
  18. package/dist/components/BenchmarkMenu.js +23 -3
  19. package/dist/components/DetailedInfoView.js +20 -0
  20. package/dist/components/DevboxActionsMenu.js +26 -62
  21. package/dist/components/DevboxCreatePage.js +763 -15
  22. package/dist/components/DevboxDetailPage.js +73 -24
  23. package/dist/components/GatewayConfigCreatePage.js +272 -0
  24. package/dist/components/LogsViewer.js +6 -40
  25. package/dist/components/ResourceDetailPage.js +143 -160
  26. package/dist/components/ResourceListView.js +3 -33
  27. package/dist/components/ResourcePicker.js +234 -0
  28. package/dist/components/SecretCreatePage.js +71 -27
  29. package/dist/components/SettingsMenu.js +12 -2
  30. package/dist/components/StateHistory.js +1 -20
  31. package/dist/components/StatusBadge.js +9 -2
  32. package/dist/components/StreamingLogsViewer.js +8 -42
  33. package/dist/components/form/FormTextInput.js +4 -2
  34. package/dist/components/resourceDetailTypes.js +18 -0
  35. package/dist/hooks/useInputHandler.js +103 -0
  36. package/dist/router/Router.js +79 -2
  37. package/dist/screens/BenchmarkDetailScreen.js +163 -0
  38. package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
  39. package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
  40. package/dist/screens/BenchmarkJobListScreen.js +479 -0
  41. package/dist/screens/BenchmarkListScreen.js +266 -0
  42. package/dist/screens/BenchmarkMenuScreen.js +6 -0
  43. package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
  44. package/dist/screens/BenchmarkRunListScreen.js +21 -1
  45. package/dist/screens/BlueprintDetailScreen.js +5 -1
  46. package/dist/screens/DevboxCreateScreen.js +2 -2
  47. package/dist/screens/GatewayConfigDetailScreen.js +236 -0
  48. package/dist/screens/GatewayConfigListScreen.js +7 -0
  49. package/dist/screens/ScenarioRunDetailScreen.js +6 -0
  50. package/dist/screens/SecretDetailScreen.js +26 -2
  51. package/dist/screens/SettingsMenuScreen.js +3 -0
  52. package/dist/screens/SnapshotDetailScreen.js +6 -0
  53. package/dist/services/agentService.js +42 -0
  54. package/dist/services/benchmarkJobService.js +122 -0
  55. package/dist/services/benchmarkService.js +47 -0
  56. package/dist/services/gatewayConfigService.js +153 -0
  57. package/dist/services/scenarioService.js +34 -0
  58. package/dist/store/benchmarkJobStore.js +66 -0
  59. package/dist/store/benchmarkStore.js +63 -0
  60. package/dist/store/gatewayConfigStore.js +83 -0
  61. package/dist/utils/browser.js +22 -0
  62. package/dist/utils/clipboard.js +41 -0
  63. package/dist/utils/commands.js +105 -9
  64. package/dist/utils/gatewayConfigValidation.js +58 -0
  65. package/dist/utils/time.js +121 -0
  66. package/package.json +43 -43
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Gateway Config Service - Handles all gateway config API calls
3
+ */
4
+ import { getClient } from "../utils/client.js";
5
+ /**
6
+ * List gateway configs with pagination
7
+ */
8
+ export async function listGatewayConfigs(options) {
9
+ const client = getClient();
10
+ const queryParams = {
11
+ limit: options.limit,
12
+ };
13
+ if (options.startingAfter) {
14
+ queryParams.starting_after = options.startingAfter;
15
+ }
16
+ if (options.search) {
17
+ queryParams.name = options.search;
18
+ }
19
+ const pagePromise = client.gatewayConfigs.list(queryParams);
20
+ const page = (await pagePromise);
21
+ const gatewayConfigs = [];
22
+ if (page.gateway_configs && Array.isArray(page.gateway_configs)) {
23
+ page.gateway_configs.forEach((g) => {
24
+ // CRITICAL: Truncate all strings to prevent Yoga crashes
25
+ const MAX_ID_LENGTH = 100;
26
+ const MAX_NAME_LENGTH = 200;
27
+ const MAX_DESC_LENGTH = 500;
28
+ const MAX_ENDPOINT_LENGTH = 500;
29
+ gatewayConfigs.push({
30
+ id: String(g.id || "").substring(0, MAX_ID_LENGTH),
31
+ name: String(g.name || "").substring(0, MAX_NAME_LENGTH),
32
+ description: g.description
33
+ ? String(g.description).substring(0, MAX_DESC_LENGTH)
34
+ : undefined,
35
+ endpoint: String(g.endpoint || "").substring(0, MAX_ENDPOINT_LENGTH),
36
+ create_time_ms: g.create_time_ms,
37
+ auth_mechanism: {
38
+ type: g.auth_mechanism.type,
39
+ key: g.auth_mechanism.key ?? undefined,
40
+ },
41
+ account_id: g.account_id ?? undefined,
42
+ });
43
+ });
44
+ }
45
+ const result = {
46
+ gatewayConfigs,
47
+ totalCount: page.total_count || gatewayConfigs.length,
48
+ hasMore: page.has_more || false,
49
+ };
50
+ return result;
51
+ }
52
+ /**
53
+ * Get a single gateway config by ID
54
+ */
55
+ export async function getGatewayConfig(id) {
56
+ const client = getClient();
57
+ const config = await client.gatewayConfigs.retrieve(id);
58
+ return {
59
+ id: config.id,
60
+ name: config.name,
61
+ description: config.description ?? undefined,
62
+ endpoint: config.endpoint,
63
+ create_time_ms: config.create_time_ms,
64
+ auth_mechanism: {
65
+ type: config.auth_mechanism.type,
66
+ key: config.auth_mechanism.key ?? undefined,
67
+ },
68
+ account_id: config.account_id ?? undefined,
69
+ };
70
+ }
71
+ /**
72
+ * Get a single gateway config by ID or name
73
+ */
74
+ export async function getGatewayConfigByIdOrName(idOrName) {
75
+ const client = getClient();
76
+ // Try to retrieve directly by ID first
77
+ try {
78
+ const config = await client.gatewayConfigs.retrieve(idOrName);
79
+ return {
80
+ id: config.id,
81
+ name: config.name,
82
+ description: config.description ?? undefined,
83
+ endpoint: config.endpoint,
84
+ create_time_ms: config.create_time_ms,
85
+ auth_mechanism: {
86
+ type: config.auth_mechanism.type,
87
+ key: config.auth_mechanism.key ?? undefined,
88
+ },
89
+ account_id: config.account_id ?? undefined,
90
+ };
91
+ }
92
+ catch {
93
+ // Not found by ID, try by name
94
+ }
95
+ // Search by name
96
+ const queryParams = {
97
+ limit: 100,
98
+ name: idOrName,
99
+ };
100
+ const pagePromise = client.gatewayConfigs.list(queryParams);
101
+ const page = (await pagePromise);
102
+ const configs = page.gateway_configs || [];
103
+ if (configs.length === 0) {
104
+ return null;
105
+ }
106
+ // Return the first exact match, or first result if no exact match
107
+ const match = configs.find((g) => g.name === idOrName) || configs[0];
108
+ return getGatewayConfig(match.id);
109
+ }
110
+ /**
111
+ * Delete a gateway config
112
+ */
113
+ export async function deleteGatewayConfig(id) {
114
+ const client = getClient();
115
+ await client.gatewayConfigs.delete(id);
116
+ }
117
+ export async function createGatewayConfig(params) {
118
+ const client = getClient();
119
+ const config = await client.gatewayConfigs.create({
120
+ name: params.name,
121
+ endpoint: params.endpoint,
122
+ auth_mechanism: params.auth_mechanism,
123
+ description: params.description,
124
+ });
125
+ return {
126
+ id: config.id,
127
+ name: config.name,
128
+ description: config.description ?? undefined,
129
+ endpoint: config.endpoint,
130
+ create_time_ms: config.create_time_ms,
131
+ auth_mechanism: {
132
+ type: config.auth_mechanism.type,
133
+ key: config.auth_mechanism.key ?? undefined,
134
+ },
135
+ account_id: config.account_id ?? undefined,
136
+ };
137
+ }
138
+ export async function updateGatewayConfig(id, params) {
139
+ const client = getClient();
140
+ const config = await client.gatewayConfigs.update(id, params);
141
+ return {
142
+ id: config.id,
143
+ name: config.name,
144
+ description: config.description ?? undefined,
145
+ endpoint: config.endpoint,
146
+ create_time_ms: config.create_time_ms,
147
+ auth_mechanism: {
148
+ type: config.auth_mechanism.type,
149
+ key: config.auth_mechanism.key ?? undefined,
150
+ },
151
+ account_id: config.account_id ?? undefined,
152
+ };
153
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Scenario Service - Handles all scenario-related API calls
3
+ */
4
+ import { getClient } from "../utils/client.js";
5
+ /**
6
+ * List scenarios with pagination
7
+ */
8
+ export async function listScenarios(options) {
9
+ const client = getClient();
10
+ const queryParams = {
11
+ limit: options.limit,
12
+ };
13
+ if (options.startingAfter) {
14
+ queryParams.starting_after = options.startingAfter;
15
+ }
16
+ // Use name filter instead of search
17
+ if (options.search) {
18
+ queryParams.name = options.search;
19
+ }
20
+ const page = await client.scenarios.list(queryParams);
21
+ const scenarios = page.scenarios || [];
22
+ return {
23
+ scenarios,
24
+ totalCount: page.total_count || scenarios.length,
25
+ hasMore: page.has_more || false,
26
+ };
27
+ }
28
+ /**
29
+ * Get scenario by ID
30
+ */
31
+ export async function getScenario(id) {
32
+ const client = getClient();
33
+ return client.scenarios.retrieve(id);
34
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Benchmark Job Store - Manages benchmark job state
3
+ */
4
+ import { create } from "zustand";
5
+ const MAX_CACHE_SIZE = 10;
6
+ export const useBenchmarkJobStore = create((set, get) => ({
7
+ // Initial state
8
+ benchmarkJobs: [],
9
+ benchmarkJobsLoading: false,
10
+ benchmarkJobsError: null,
11
+ benchmarkJobsTotalCount: 0,
12
+ benchmarkJobsHasMore: false,
13
+ benchmarkJobsCurrentPage: 0,
14
+ // Selection
15
+ selectedBenchmarkJobIndex: 0,
16
+ // Cache
17
+ benchmarkJobPageCache: new Map(),
18
+ // Actions
19
+ setBenchmarkJobs: (jobs) => set({ benchmarkJobs: jobs }),
20
+ setBenchmarkJobsLoading: (loading) => set({ benchmarkJobsLoading: loading }),
21
+ setBenchmarkJobsError: (error) => set({ benchmarkJobsError: error }),
22
+ setBenchmarkJobsTotalCount: (count) => set({ benchmarkJobsTotalCount: count }),
23
+ setBenchmarkJobsHasMore: (hasMore) => set({ benchmarkJobsHasMore: hasMore }),
24
+ setBenchmarkJobsCurrentPage: (page) => set({ benchmarkJobsCurrentPage: page }),
25
+ setSelectedBenchmarkJobIndex: (index) => set({ selectedBenchmarkJobIndex: index }),
26
+ // Cache management
27
+ cacheBenchmarkJobPage: (page, data) => {
28
+ const state = get();
29
+ const cache = state.benchmarkJobPageCache;
30
+ if (cache.size >= MAX_CACHE_SIZE) {
31
+ const oldestKey = cache.keys().next().value;
32
+ if (oldestKey !== undefined) {
33
+ cache.delete(oldestKey);
34
+ }
35
+ }
36
+ const plainData = data.map((d) => JSON.parse(JSON.stringify(d)));
37
+ cache.set(page, plainData);
38
+ set({});
39
+ },
40
+ getCachedBenchmarkJobPage: (page) => {
41
+ return get().benchmarkJobPageCache.get(page);
42
+ },
43
+ clearCache: () => {
44
+ const state = get();
45
+ state.benchmarkJobPageCache.clear();
46
+ set({ benchmarkJobPageCache: new Map() });
47
+ },
48
+ clearAll: () => {
49
+ const state = get();
50
+ state.benchmarkJobPageCache.clear();
51
+ set({
52
+ benchmarkJobs: [],
53
+ benchmarkJobsLoading: false,
54
+ benchmarkJobsError: null,
55
+ benchmarkJobsTotalCount: 0,
56
+ benchmarkJobsHasMore: false,
57
+ benchmarkJobsCurrentPage: 0,
58
+ selectedBenchmarkJobIndex: 0,
59
+ benchmarkJobPageCache: new Map(),
60
+ });
61
+ },
62
+ getSelectedBenchmarkJob: () => {
63
+ const state = get();
64
+ return state.benchmarkJobs[state.selectedBenchmarkJobIndex];
65
+ },
66
+ }));
@@ -4,6 +4,15 @@
4
4
  import { create } from "zustand";
5
5
  const MAX_CACHE_SIZE = 10;
6
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(),
7
16
  // Initial benchmark run state
8
17
  benchmarkRuns: [],
9
18
  benchmarkRunsLoading: false,
@@ -24,8 +33,30 @@ export const useBenchmarkStore = create((set, get) => ({
24
33
  selectedBenchmarkRunIndex: 0,
25
34
  selectedScenarioRunIndex: 0,
26
35
  // Caches
36
+ benchmarkPageCache: new Map(),
27
37
  benchmarkRunPageCache: new Map(),
28
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() }),
29
60
  // Benchmark Run Actions
30
61
  setBenchmarkRuns: (runs) => set({ benchmarkRuns: runs }),
31
62
  setBenchmarkRunsLoading: (loading) => set({ benchmarkRunsLoading: loading }),
@@ -44,6 +75,22 @@ export const useBenchmarkStore = create((set, get) => ({
44
75
  setSelectedScenarioRunIndex: (index) => set({ selectedScenarioRunIndex: index }),
45
76
  setBenchmarkRunIdFilter: (id) => set({ benchmarkRunIdFilter: id }),
46
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
+ },
47
94
  cacheBenchmarkRunPage: (page, data) => {
48
95
  const state = get();
49
96
  const cache = state.benchmarkRunPageCache;
@@ -78,18 +125,29 @@ export const useBenchmarkStore = create((set, get) => ({
78
125
  },
79
126
  clearCache: () => {
80
127
  const state = get();
128
+ state.benchmarkPageCache.clear();
81
129
  state.benchmarkRunPageCache.clear();
82
130
  state.scenarioRunPageCache.clear();
83
131
  set({
132
+ benchmarkPageCache: new Map(),
84
133
  benchmarkRunPageCache: new Map(),
85
134
  scenarioRunPageCache: new Map(),
86
135
  });
87
136
  },
88
137
  clearAll: () => {
89
138
  const state = get();
139
+ state.benchmarkPageCache.clear();
90
140
  state.benchmarkRunPageCache.clear();
91
141
  state.scenarioRunPageCache.clear();
92
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(),
93
151
  benchmarkRuns: [],
94
152
  benchmarkRunsLoading: false,
95
153
  benchmarkRunsError: null,
@@ -105,10 +163,15 @@ export const useBenchmarkStore = create((set, get) => ({
105
163
  benchmarkRunIdFilter: undefined,
106
164
  selectedBenchmarkRunIndex: 0,
107
165
  selectedScenarioRunIndex: 0,
166
+ benchmarkPageCache: new Map(),
108
167
  benchmarkRunPageCache: new Map(),
109
168
  scenarioRunPageCache: new Map(),
110
169
  });
111
170
  },
171
+ getSelectedBenchmark: () => {
172
+ const state = get();
173
+ return state.benchmarks[state.selectedBenchmarkIndex];
174
+ },
112
175
  getSelectedBenchmarkRun: () => {
113
176
  const state = get();
114
177
  return state.benchmarkRuns[state.selectedBenchmarkRunIndex];
@@ -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
+ }));
@@ -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
+ }
@@ -14,7 +14,9 @@ export function createProgram() {
14
14
  program
15
15
  .name("rli")
16
16
  .description("Beautiful CLI for Runloop devbox management")
17
- .version(VERSION);
17
+ .version(VERSION)
18
+ .showHelpAfterError()
19
+ .showSuggestionAfterError();
18
20
  // Devbox commands
19
21
  const devbox = program
20
22
  .command("devbox")
@@ -40,6 +42,8 @@ export function createProgram() {
40
42
  .option("--root", "Run as root")
41
43
  .option("--user <user:uid>", "Run as this user (format: username:uid)")
42
44
  .option("--network-policy <id>", "Network policy ID to apply")
45
+ .option("--tunnel <mode>", "Tunnel authentication mode (open, authenticated)")
46
+ .option("--gateways <gateways...>", "Gateway configurations (format: ENV_PREFIX=gateway_id_or_name,secret_id_or_name)")
43
47
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
44
48
  .action(createDevbox);
45
49
  devbox
@@ -117,22 +121,36 @@ export function createProgram() {
117
121
  await sshDevbox(id, options);
118
122
  });
119
123
  devbox
120
- .command("scp <id> <src> <dst>")
121
- .description("Copy files to/from a devbox using scp")
124
+ .command("scp <src> <dst>")
125
+ .description("Copy files to/from a devbox using scp. Use the devbox ID (dbx_*) as a hostname in src or dst.\n\n" +
126
+ " Examples:\n" +
127
+ " $ rli devbox scp dbx_abc123:/home/user/file.txt ./file.txt # download from devbox\n" +
128
+ " $ rli devbox scp ./file.txt dbx_abc123:/home/user/file.txt # upload to devbox\n" +
129
+ " $ rli devbox scp root@dbx_abc123:/etc/hosts ./hosts # with explicit user\n" +
130
+ " $ rli devbox scp dbx_src:/data/file.txt dbx_dst:/data/file.txt # devbox to devbox\n\n" +
131
+ " If no user is specified, the devbox's configured user is used.\n" +
132
+ " Paths without a dbx_ hostname are treated as local.\n" +
133
+ " Devbox-to-devbox transfers route through your local machine via scp -3.")
122
134
  .option("--scp-options <options>", "Additional scp options (quoted)")
123
135
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
124
- .action(async (id, src, dst, options) => {
136
+ .action(async (src, dst, options) => {
125
137
  const { scpFiles } = await import("../commands/devbox/scp.js");
126
- await scpFiles(id, { src, dst, ...options });
138
+ await scpFiles(src, dst, options);
127
139
  });
128
140
  devbox
129
- .command("rsync <id> <src> <dst>")
130
- .description("Sync files to/from a devbox using rsync")
141
+ .command("rsync <src> <dst>")
142
+ .description("Sync files to/from a devbox using rsync. Use the devbox ID (dbx_*) as a hostname in src or dst.\n\n" +
143
+ " Examples:\n" +
144
+ " $ rli devbox rsync dbx_abc123:/home/user/data/ ./data/ # download from devbox\n" +
145
+ " $ rli devbox rsync ./data/ dbx_abc123:/home/user/data/ # upload to devbox\n" +
146
+ " $ rli devbox rsync root@dbx_abc123:/etc/config/ ./config/ # with explicit user\n\n" +
147
+ " If no user is specified, the devbox's configured user is used.\n" +
148
+ " Paths without a dbx_ hostname are treated as local.")
131
149
  .option("--rsync-options <options>", "Additional rsync options (quoted)")
132
150
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
133
- .action(async (id, src, dst, options) => {
151
+ .action(async (src, dst, options) => {
134
152
  const { rsyncFiles } = await import("../commands/devbox/rsync.js");
135
- await rsyncFiles(id, { src, dst, ...options });
153
+ await rsyncFiles(src, dst, options);
136
154
  });
137
155
  devbox
138
156
  .command("tunnel <id> <ports>")
@@ -248,6 +266,17 @@ export function createProgram() {
248
266
  const { getSnapshot } = await import("../commands/snapshot/get.js");
249
267
  await getSnapshot({ id, ...options });
250
268
  });
269
+ snapshot
270
+ .command("prune <devbox-id>")
271
+ .description("Delete old snapshots for a devbox, keeping only recent ready ones")
272
+ .option("--dry-run", "Show what would be deleted without actually deleting")
273
+ .option("-y, --yes", "Skip confirmation prompt")
274
+ .option("--keep <n>", "Number of ready snapshots to keep", "1")
275
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
276
+ .action(async (devboxId, options) => {
277
+ const { pruneSnapshots } = await import("../commands/snapshot/prune.js");
278
+ await pruneSnapshots(devboxId, options);
279
+ });
251
280
  snapshot
252
281
  .command("status <snapshot-id>")
253
282
  .description("Get snapshot operation status")
@@ -303,6 +332,15 @@ export function createProgram() {
303
332
  const { getBlueprintLogs } = await import("../commands/blueprint/logs.js");
304
333
  await getBlueprintLogs({ id, ...options });
305
334
  });
335
+ blueprint
336
+ .command("delete <id>")
337
+ .description("Delete a blueprint by ID")
338
+ .alias("rm")
339
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
340
+ .action(async (id, options) => {
341
+ const { deleteBlueprint } = await import("../commands/blueprint/delete.js");
342
+ await deleteBlueprint(id, options);
343
+ });
306
344
  blueprint
307
345
  .command("prune <name>")
308
346
  .description("Delete old blueprint builds, keeping only recent successful ones")
@@ -488,6 +526,64 @@ export function createProgram() {
488
526
  const { deleteSecret } = await import("../commands/secret/delete.js");
489
527
  await deleteSecret(name, options);
490
528
  });
529
+ // Gateway config commands
530
+ const gatewayConfig = program
531
+ .command("gateway-config")
532
+ .description("Manage gateway configurations")
533
+ .alias("gwc");
534
+ gatewayConfig
535
+ .command("list")
536
+ .description("List gateway configurations")
537
+ .option("--name <name>", "Filter by name")
538
+ .option("--limit <n>", "Max results", "20")
539
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
540
+ .action(async (options) => {
541
+ const { listGatewayConfigs } = await import("../commands/gateway-config/list.js");
542
+ await listGatewayConfigs(options);
543
+ });
544
+ gatewayConfig
545
+ .command("create")
546
+ .description("Create a new gateway configuration")
547
+ .requiredOption("--name <name>", "Gateway config name (required)")
548
+ .requiredOption("--endpoint <url>", "Target endpoint URL (required)")
549
+ .option("--bearer-auth", "Use Bearer token authentication (default)")
550
+ .option("--header-auth <header>", "Use custom header authentication (specify header key name)")
551
+ .option("--description <description>", "Description")
552
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
553
+ .action(async (options) => {
554
+ const { createGatewayConfig } = await import("../commands/gateway-config/create.js");
555
+ await createGatewayConfig(options);
556
+ });
557
+ gatewayConfig
558
+ .command("get <id>")
559
+ .description("Get gateway configuration details")
560
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
561
+ .action(async (id, options) => {
562
+ const { getGatewayConfig } = await import("../commands/gateway-config/get.js");
563
+ await getGatewayConfig({ id, ...options });
564
+ });
565
+ gatewayConfig
566
+ .command("update <id>")
567
+ .description("Update a gateway configuration")
568
+ .option("--name <name>", "New name")
569
+ .option("--endpoint <url>", "New endpoint URL")
570
+ .option("--bearer-auth", "Use Bearer token authentication")
571
+ .option("--header-auth <header>", "Use custom header authentication (specify header key name)")
572
+ .option("--description <description>", "New description")
573
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
574
+ .action(async (id, options) => {
575
+ const { updateGatewayConfig } = await import("../commands/gateway-config/update.js");
576
+ await updateGatewayConfig({ id, ...options });
577
+ });
578
+ gatewayConfig
579
+ .command("delete <id>")
580
+ .description("Delete a gateway configuration")
581
+ .alias("rm")
582
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
583
+ .action(async (id, options) => {
584
+ const { deleteGatewayConfig } = await import("../commands/gateway-config/delete.js");
585
+ await deleteGatewayConfig(id, options);
586
+ });
491
587
  // MCP server commands
492
588
  const mcp = program
493
589
  .command("mcp")