@runloop/rl-cli 1.9.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.
package/README.md CHANGED
@@ -92,8 +92,8 @@ rli devbox suspend <id> # Suspend a devbox
92
92
  rli devbox resume <id> # Resume a suspended devbox
93
93
  rli devbox shutdown <id> # Shutdown a devbox
94
94
  rli devbox ssh <id> # SSH into a devbox
95
- rli devbox scp <id> <src> <dst> # Copy files to/from a devbox using scp
96
- rli devbox rsync <id> <src> <dst> # Sync files to/from a devbox using rsync
95
+ rli devbox scp <src> <dst> # Copy files to/from a devbox using scp...
96
+ rli devbox rsync <src> <dst> # Sync files to/from a devbox using rsy...
97
97
  rli devbox tunnel <id> <ports> # Create a port-forwarding tunnel to a ...
98
98
  rli devbox read <id> # Read a file from a devbox using the API
99
99
  rli devbox write <id> # Write a file to a devbox using the API
@@ -304,8 +304,10 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
304
304
  const startIndex = currentPage * PAGE_SIZE;
305
305
  const endIndex = startIndex + devboxes.length;
306
306
  // Filter operations based on devbox status
307
+ const hasTunnel = !!(selectedDevbox?.tunnel && selectedDevbox.tunnel.tunnel_key);
307
308
  const operations = selectedDevbox
308
- ? allOperations.filter((op) => {
309
+ ? allOperations
310
+ .filter((op) => {
309
311
  const devboxStatus = selectedDevbox.status;
310
312
  if (devboxStatus === "suspended") {
311
313
  return op.key === "resume" || op.key === "logs";
@@ -319,6 +321,20 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
319
321
  return op.key !== "resume";
320
322
  }
321
323
  return op.key === "logs" || op.key === "delete";
324
+ })
325
+ .map((op) => {
326
+ // Dynamic tunnel label based on whether tunnel is active
327
+ if (op.key === "tunnel") {
328
+ return hasTunnel
329
+ ? {
330
+ ...op,
331
+ label: "Tunnel (Active)",
332
+ color: colors.success,
333
+ icon: figures.tick,
334
+ }
335
+ : op;
336
+ }
337
+ return op;
322
338
  })
323
339
  : allOperations;
324
340
  const closePopup = React.useCallback(() => {
@@ -1,62 +1,90 @@
1
1
  /**
2
2
  * Rsync files to/from devbox command
3
+ *
4
+ * Supports standard rsync-like syntax where the devbox ID (dbx_*) is used as a hostname:
5
+ * rli devbox rsync dbx_abc123:/remote/path ./local/path # download
6
+ * rli devbox rsync ./local/path dbx_abc123:/remote/path # upload
7
+ * rli devbox rsync root@dbx_abc123:/remote/path ./local/path # explicit user
8
+ *
9
+ * If no user is specified for a remote path, the devbox's configured user is used.
10
+ * Paths without a dbx_ hostname are treated as local paths.
3
11
  */
4
- import { exec } from "child_process";
12
+ import { execFile } from "child_process";
5
13
  import { promisify } from "util";
6
- import { getClient } from "../../utils/client.js";
7
14
  import { output, outputError } from "../../utils/output.js";
8
- import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
9
- const execAsync = promisify(exec);
10
- export async function rsyncFiles(devboxId, options) {
15
+ import { getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
16
+ import { parseSCPPath, resolveRemote } from "./scp.js";
17
+ const execFileAsync = promisify(execFile);
18
+ /**
19
+ * Build the rsync command for a single-remote transfer (local <-> devbox).
20
+ */
21
+ export function buildRsyncCommand(opts) {
22
+ // Rsync re-splits the -e value on whitespace internally, so the
23
+ // ProxyCommand (which contains spaces) must be single-quoted.
24
+ const sshTransport = `ssh -i ${opts.sshInfo.keyfilePath} -o 'ProxyCommand=${opts.proxyCommand}' -o StrictHostKeyChecking=no`;
25
+ const rsyncCommand = [
26
+ "rsync",
27
+ "-vrz", // v: verbose, r: recursive, z: compress
28
+ "-e",
29
+ sshTransport,
30
+ ];
31
+ if (opts.rsyncOptions) {
32
+ rsyncCommand.push(...opts.rsyncOptions.split(" "));
33
+ }
34
+ // Build src argument
35
+ if (opts.parsedSrc.isRemote) {
36
+ const user = opts.parsedSrc.user || opts.defaultUser;
37
+ rsyncCommand.push(`${user}@${opts.sshInfo.url}:${opts.parsedSrc.path}`);
38
+ }
39
+ else {
40
+ rsyncCommand.push(opts.parsedSrc.path);
41
+ }
42
+ // Build dst argument
43
+ if (opts.parsedDst.isRemote) {
44
+ const user = opts.parsedDst.user || opts.defaultUser;
45
+ rsyncCommand.push(`${user}@${opts.sshInfo.url}:${opts.parsedDst.path}`);
46
+ }
47
+ else {
48
+ rsyncCommand.push(opts.parsedDst.path);
49
+ }
50
+ return rsyncCommand;
51
+ }
52
+ export async function rsyncFiles(src, dst, options) {
11
53
  try {
12
54
  // Check if SSH tools are available
13
55
  const sshToolsAvailable = await checkSSHTools();
14
56
  if (!sshToolsAvailable) {
15
57
  outputError("SSH tools (ssh, rsync, openssl) are not available on this system");
16
58
  }
17
- const client = getClient();
18
- // Get devbox details to determine user
19
- const devbox = await client.devboxes.retrieve(devboxId);
20
- const user = devbox.launch_parameters?.user_parameters?.username || "user";
21
- // Get SSH key
22
- const sshInfo = await getSSHKey(devboxId);
23
- if (!sshInfo) {
24
- outputError("Failed to create SSH key");
25
- }
26
- const proxyCommand = getProxyCommand();
27
- const sshOptions = `-i ${sshInfo.keyfilePath} -o ProxyCommand='${proxyCommand}' -o StrictHostKeyChecking=no`;
28
- const rsyncCommand = [
29
- "rsync",
30
- "-vrz", // v: verbose, r: recursive, z: compress
31
- "-e",
32
- `"ssh ${sshOptions}"`,
33
- ];
34
- if (options.rsyncOptions) {
35
- rsyncCommand.push(...options.rsyncOptions.split(" "));
36
- }
37
- // Handle remote paths (starting with :)
38
- if (options.src.startsWith(":")) {
39
- rsyncCommand.push(`${user}@${sshInfo.url}:${options.src.slice(1)}`);
40
- rsyncCommand.push(options.dst);
59
+ const parsedSrc = parseSCPPath(src);
60
+ const parsedDst = parseSCPPath(dst);
61
+ if (!parsedSrc.isRemote && !parsedDst.isRemote) {
62
+ outputError("At least one of src or dst must be a remote devbox path (e.g. dbx_<id>:/path)");
41
63
  }
42
- else {
43
- rsyncCommand.push(options.src);
44
- if (options.dst.startsWith(":")) {
45
- rsyncCommand.push(`${user}@${sshInfo.url}:${options.dst.slice(1)}`);
46
- }
47
- else {
48
- rsyncCommand.push(options.dst);
49
- }
64
+ if (parsedSrc.isRemote && parsedDst.isRemote) {
65
+ outputError("Devbox-to-devbox rsync is not supported. Only one side can be a remote devbox path.");
50
66
  }
51
- await execAsync(rsyncCommand.join(" "));
67
+ const devboxId = parsedSrc.isRemote ? parsedSrc.host : parsedDst.host;
68
+ const remote = await resolveRemote(devboxId);
69
+ const proxyCommand = getProxyCommand();
70
+ const rsyncCommand = buildRsyncCommand({
71
+ sshInfo: remote.sshInfo,
72
+ proxyCommand,
73
+ parsedSrc,
74
+ parsedDst,
75
+ defaultUser: remote.defaultUser,
76
+ rsyncOptions: options.rsyncOptions,
77
+ });
78
+ const [cmd, ...args] = rsyncCommand;
79
+ await execFileAsync(cmd, args);
52
80
  // Default: just output the destination for easy scripting
53
81
  if (!options.output || options.output === "text") {
54
- console.log(options.dst);
82
+ console.log(dst);
55
83
  }
56
84
  else {
57
85
  output({
58
- source: options.src,
59
- destination: options.dst,
86
+ source: src,
87
+ destination: dst,
60
88
  }, { format: options.output, defaultFormat: "json" });
61
89
  }
62
90
  }
@@ -1,64 +1,205 @@
1
1
  /**
2
2
  * SCP files to/from devbox command
3
+ *
4
+ * Supports standard SCP-like syntax where the devbox ID (dbx_*) is used as a hostname:
5
+ * rli devbox scp dbx_abc123:/remote/path ./local/path # download
6
+ * rli devbox scp ./local/path dbx_abc123:/remote/path # upload
7
+ * rli devbox scp root@dbx_abc123:/remote/path ./local/path # explicit user
8
+ * rli devbox scp dbx_src:/file dbx_dst:/file # devbox-to-devbox
9
+ *
10
+ * If no user is specified for a remote path, the devbox's configured user is used.
11
+ * Paths without a dbx_ hostname are treated as local paths.
12
+ *
13
+ * Devbox-to-devbox transfers use scp -3 to route data through the local machine,
14
+ * with a temporary SSH config so each devbox uses its own key.
3
15
  */
4
- import { exec } from "child_process";
16
+ import { execFile } from "child_process";
5
17
  import { promisify } from "util";
18
+ import { writeFile, unlink } from "fs/promises";
19
+ import { join } from "path";
20
+ import { tmpdir } from "os";
21
+ import { randomUUID } from "crypto";
6
22
  import { getClient } from "../../utils/client.js";
7
23
  import { output, outputError } from "../../utils/output.js";
8
- import { getSSHKey, getProxyCommand, checkSSHTools } from "../../utils/ssh.js";
9
- const execAsync = promisify(exec);
10
- export async function scpFiles(devboxId, options) {
24
+ import { getSSHKey, getProxyCommand, checkSSHTools, } from "../../utils/ssh.js";
25
+ const execFileAsync = promisify(execFile);
26
+ /**
27
+ * Parse an SCP-style path into its components.
28
+ *
29
+ * Supported formats:
30
+ * user@dbx_id:path -> remote with explicit user
31
+ * dbx_id:path -> remote with default user
32
+ * /local/path -> local (absolute)
33
+ * ./relative -> local (relative)
34
+ * filename -> local (bare filename, no colon)
35
+ */
36
+ export function parseSCPPath(input) {
37
+ // Match [user@]host:path where host is a devbox ID (dbx_*).
38
+ // This avoids false positives on local paths that happen to contain colons.
39
+ const match = input.match(/^(?:([^@/:]+)@)?(dbx_[^@/:]+):(.*)$/);
40
+ if (match) {
41
+ return {
42
+ user: match[1] || undefined,
43
+ host: match[2],
44
+ path: match[3],
45
+ isRemote: true,
46
+ };
47
+ }
48
+ return { path: input, isRemote: false };
49
+ }
50
+ /**
51
+ * Resolve a devbox ID to its SSH info and default user.
52
+ */
53
+ export async function resolveRemote(devboxId) {
54
+ const client = getClient();
55
+ const devbox = await client.devboxes.retrieve(devboxId);
56
+ const defaultUser = devbox.launch_parameters?.user_parameters?.username || "user";
57
+ const sshInfo = await getSSHKey(devboxId);
58
+ if (!sshInfo) {
59
+ throw new Error(`Failed to create SSH key for ${devboxId}`);
60
+ }
61
+ return { devboxId, defaultUser, sshInfo };
62
+ }
63
+ /**
64
+ * Build the SCP command for a single-remote transfer (local <-> devbox).
65
+ */
66
+ export function buildSCPCommand(opts) {
67
+ const scpCommand = [
68
+ "scp",
69
+ "-i",
70
+ opts.sshInfo.keyfilePath,
71
+ "-o",
72
+ `ProxyCommand=${opts.proxyCommand}`,
73
+ "-o",
74
+ "StrictHostKeyChecking=no",
75
+ ];
76
+ if (opts.scpOptions) {
77
+ scpCommand.push(...opts.scpOptions.split(" "));
78
+ }
79
+ // Build src argument
80
+ if (opts.parsedSrc.isRemote) {
81
+ const user = opts.parsedSrc.user || opts.defaultUser;
82
+ scpCommand.push(`${user}@${opts.sshInfo.url}:${opts.parsedSrc.path}`);
83
+ }
84
+ else {
85
+ scpCommand.push(opts.parsedSrc.path);
86
+ }
87
+ // Build dst argument
88
+ if (opts.parsedDst.isRemote) {
89
+ const user = opts.parsedDst.user || opts.defaultUser;
90
+ scpCommand.push(`${user}@${opts.sshInfo.url}:${opts.parsedDst.path}`);
91
+ }
92
+ else {
93
+ scpCommand.push(opts.parsedDst.path);
94
+ }
95
+ return scpCommand;
96
+ }
97
+ /**
98
+ * Build the SCP command for a dual-remote transfer (devbox -> devbox).
99
+ * Uses scp -3 to route data through the local machine and a temporary
100
+ * SSH config file so each devbox resolves to its own key/proxy.
101
+ */
102
+ export function buildDualRemoteSCPCommand(opts) {
103
+ const scpCommand = [
104
+ "scp",
105
+ "-3",
106
+ "-F",
107
+ opts.sshConfigPath,
108
+ "-o",
109
+ "StrictHostKeyChecking=no",
110
+ ];
111
+ if (opts.scpOptions) {
112
+ scpCommand.push(...opts.scpOptions.split(" "));
113
+ }
114
+ const srcUser = opts.parsedSrc.user || opts.srcRemote.defaultUser;
115
+ scpCommand.push(`${srcUser}@${opts.srcRemote.sshInfo.url}:${opts.parsedSrc.path}`);
116
+ const dstUser = opts.parsedDst.user || opts.dstRemote.defaultUser;
117
+ scpCommand.push(`${dstUser}@${opts.dstRemote.sshInfo.url}:${opts.parsedDst.path}`);
118
+ return scpCommand;
119
+ }
120
+ /**
121
+ * Generate a temporary SSH config file for dual-remote transfers.
122
+ * Maps each devbox URL to its identity file and proxy command.
123
+ */
124
+ export function generateSCPConfig(remotes, proxyCommand) {
125
+ return remotes
126
+ .map((r) => `Host ${r.sshInfo.url}\n` +
127
+ ` IdentityFile ${r.sshInfo.keyfilePath}\n` +
128
+ ` ProxyCommand ${proxyCommand}\n` +
129
+ ` StrictHostKeyChecking no`)
130
+ .join("\n\n");
131
+ }
132
+ export async function scpFiles(src, dst, options) {
11
133
  try {
12
134
  // Check if SSH tools are available
13
135
  const sshToolsAvailable = await checkSSHTools();
14
136
  if (!sshToolsAvailable) {
15
137
  outputError("SSH tools (ssh, scp, openssl) are not available on this system");
16
138
  }
17
- const client = getClient();
18
- // Get devbox details to determine user
19
- const devbox = await client.devboxes.retrieve(devboxId);
20
- const user = devbox.launch_parameters?.user_parameters?.username || "user";
21
- // Get SSH key
22
- const sshInfo = await getSSHKey(devboxId);
23
- if (!sshInfo) {
24
- outputError("Failed to create SSH key");
139
+ const parsedSrc = parseSCPPath(src);
140
+ const parsedDst = parseSCPPath(dst);
141
+ if (!parsedSrc.isRemote && !parsedDst.isRemote) {
142
+ outputError("At least one of src or dst must be a remote devbox path (e.g. dbx_<id>:/path)");
25
143
  }
26
144
  const proxyCommand = getProxyCommand();
27
- const scpCommand = [
28
- "scp",
29
- "-i",
30
- sshInfo.keyfilePath,
31
- "-o",
32
- `ProxyCommand=${proxyCommand}`,
33
- "-o",
34
- "StrictHostKeyChecking=no",
35
- ];
36
- if (options.scpOptions) {
37
- scpCommand.push(...options.scpOptions.split(" "));
38
- }
39
- // Handle remote paths (starting with :)
40
- if (options.src.startsWith(":")) {
41
- scpCommand.push(`${user}@${sshInfo.url}:${options.src.slice(1)}`);
42
- scpCommand.push(options.dst);
43
- }
44
- else {
45
- scpCommand.push(options.src);
46
- if (options.dst.startsWith(":")) {
47
- scpCommand.push(`${user}@${sshInfo.url}:${options.dst.slice(1)}`);
145
+ let scpCommand;
146
+ if (parsedSrc.isRemote && parsedDst.isRemote) {
147
+ // Both sides are remote devboxes — resolve both in parallel
148
+ const [srcRemote, dstRemote] = await Promise.all([
149
+ resolveRemote(parsedSrc.host),
150
+ resolveRemote(parsedDst.host),
151
+ ]);
152
+ // Write a temporary SSH config so scp can find the right key per host
153
+ const configContent = generateSCPConfig([srcRemote, dstRemote], proxyCommand);
154
+ const configPath = join(tmpdir(), `rli-scp-${randomUUID()}.conf`);
155
+ const configHeader = "# Temporary SSH config generated by `rli devbox scp` for a dual-remote transfer.\n" +
156
+ "# Safe to delete.\n\n";
157
+ await writeFile(configPath, configHeader + configContent, {
158
+ mode: 0o600,
159
+ });
160
+ try {
161
+ scpCommand = buildDualRemoteSCPCommand({
162
+ srcRemote,
163
+ dstRemote,
164
+ proxyCommand,
165
+ parsedSrc,
166
+ parsedDst,
167
+ sshConfigPath: configPath,
168
+ scpOptions: options.scpOptions,
169
+ });
170
+ const [cmd, ...args] = scpCommand;
171
+ await execFileAsync(cmd, args);
48
172
  }
49
- else {
50
- scpCommand.push(options.dst);
173
+ finally {
174
+ // Clean up the temp SSH config file.
175
+ // Note: SSH key files in ~/.runloop/ssh_keys/ are intentionally kept —
176
+ // they're shared across rli commands (ssh, scp, etc.).
177
+ await unlink(configPath).catch(() => { });
51
178
  }
52
179
  }
53
- await execAsync(scpCommand.join(" "));
180
+ else {
181
+ // Single remote — one side is local
182
+ const devboxId = parsedSrc.isRemote ? parsedSrc.host : parsedDst.host;
183
+ const remote = await resolveRemote(devboxId);
184
+ scpCommand = buildSCPCommand({
185
+ sshInfo: remote.sshInfo,
186
+ proxyCommand,
187
+ parsedSrc,
188
+ parsedDst,
189
+ defaultUser: remote.defaultUser,
190
+ scpOptions: options.scpOptions,
191
+ });
192
+ const [cmd, ...args] = scpCommand;
193
+ await execFileAsync(cmd, args);
194
+ }
54
195
  // Default: just output the destination for easy scripting
55
196
  if (!options.output || options.output === "text") {
56
- console.log(options.dst);
197
+ console.log(dst);
57
198
  }
58
199
  else {
59
200
  output({
60
- source: options.src,
61
- destination: options.dst,
201
+ source: src,
202
+ destination: dst,
62
203
  }, { format: options.output, defaultFormat: "json" });
63
204
  }
64
205
  }
@@ -3,32 +3,41 @@
3
3
  */
4
4
  import { getClient } from "../../utils/client.js";
5
5
  import { output, outputError } from "../../utils/output.js";
6
+ import { validateGatewayConfig } from "../../utils/gatewayConfigValidation.js";
6
7
  export async function createGatewayConfig(options) {
7
8
  try {
8
9
  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'");
10
+ // Validate that exactly one auth type is specified
11
+ if (options.bearerAuth && options.headerAuth) {
12
+ outputError("Cannot specify both --bearer-auth and --header-auth. Choose one.");
13
13
  return;
14
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'");
15
+ // Default to bearer if neither is specified
16
+ const authType = options.headerAuth ? "header" : "bearer";
17
+ // Validate all fields using shared validation
18
+ const validation = validateGatewayConfig({
19
+ name: options.name,
20
+ endpoint: options.endpoint,
21
+ authType,
22
+ authKey: options.headerAuth,
23
+ }, { requireName: true, requireEndpoint: true });
24
+ if (!validation.valid) {
25
+ outputError(validation.errors.join("\n"));
18
26
  return;
19
27
  }
28
+ const { sanitized } = validation;
20
29
  // Build auth mechanism
21
30
  const authMechanism = {
22
- type: authType,
31
+ type: sanitized.authType,
23
32
  };
24
- if (authType === "header" && options.authKey) {
25
- authMechanism.key = options.authKey;
33
+ if (sanitized.authType === "header" && sanitized.authKey) {
34
+ authMechanism.key = sanitized.authKey;
26
35
  }
27
36
  const config = await client.gatewayConfigs.create({
28
- name: options.name,
29
- endpoint: options.endpoint,
37
+ name: sanitized.name,
38
+ endpoint: sanitized.endpoint,
30
39
  auth_mechanism: authMechanism,
31
- description: options.description,
40
+ description: options.description?.trim() || undefined,
32
41
  });
33
42
  // Default: just output the ID for easy scripting
34
43
  if (!options.output || options.output === "text") {
@@ -1,12 +1,15 @@
1
1
  /**
2
- * Get gateway config command
2
+ * Get gateway config command - supports lookup by ID or name
3
3
  */
4
- import { getClient } from "../../utils/client.js";
4
+ import { getGatewayConfigByIdOrName } from "../../services/gatewayConfigService.js";
5
5
  import { output, outputError } from "../../utils/output.js";
6
6
  export async function getGatewayConfig(options) {
7
7
  try {
8
- const client = getClient();
9
- const config = await client.gatewayConfigs.retrieve(options.id);
8
+ const config = await getGatewayConfigByIdOrName(options.id);
9
+ if (!config) {
10
+ outputError(`Gateway config not found: ${options.id}`);
11
+ return;
12
+ }
10
13
  output(config, { format: options.output, defaultFormat: "json" });
11
14
  }
12
15
  catch (error) {
@@ -138,13 +138,13 @@ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
138
138
  },
139
139
  {
140
140
  key: "edit",
141
- label: "Edit Gateway Config",
141
+ label: "Edit AI Gateway Config",
142
142
  color: colors.warning,
143
143
  icon: figures.pointer,
144
144
  },
145
145
  {
146
146
  key: "delete",
147
- label: "Delete Gateway Config",
147
+ label: "Delete AI Gateway Config",
148
148
  color: colors.error,
149
149
  icon: figures.cross,
150
150
  },
@@ -205,7 +205,7 @@ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
205
205
  switch (operationKey) {
206
206
  case "delete":
207
207
  await client.gatewayConfigs.delete(config.id);
208
- setOperationResult(`Gateway config "${config.name}" deleted successfully`);
208
+ setOperationResult(`AI gateway config "${config.name}" deleted successfully`);
209
209
  break;
210
210
  }
211
211
  }
@@ -375,8 +375,8 @@ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
375
375
  });
376
376
  // Delete confirmation
377
377
  if (showDeleteConfirm && selectedConfig) {
378
- return (_jsx(ConfirmationPrompt, { title: "Delete Gateway Config", message: `Are you sure you want to delete "${selectedConfig.name}"?`, details: "This action cannot be undone. Any devboxes using this gateway config will no longer have access to it.", breadcrumbItems: [
379
- { label: "Gateway Configs" },
378
+ return (_jsx(ConfirmationPrompt, { title: "Delete AI Gateway Config", message: `Are you sure you want to delete "${selectedConfig.name}"?`, details: "This action cannot be undone. Any devboxes using this AI gateway config will no longer have access to it.", breadcrumbItems: [
379
+ { label: "AI Gateway Configs" },
380
380
  { label: selectedConfig.name || selectedConfig.id },
381
381
  { label: "Delete", active: true },
382
382
  ], onConfirm: () => {
@@ -393,7 +393,7 @@ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
393
393
  const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
394
394
  "Operation";
395
395
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
396
- { label: "Gateway Configs" },
396
+ { label: "AI Gateway Configs" },
397
397
  {
398
398
  label: selectedConfig?.name || selectedConfig?.id || "Config",
399
399
  },
@@ -405,10 +405,10 @@ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
405
405
  const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
406
406
  "Operation";
407
407
  const messages = {
408
- delete: "Deleting gateway config...",
408
+ delete: "Deleting AI gateway config...",
409
409
  };
410
410
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
411
- { label: "Gateway Configs" },
411
+ { label: "AI Gateway Configs" },
412
412
  { label: selectedConfig.name || selectedConfig.id },
413
413
  { label: operationLabel, active: true },
414
414
  ] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
@@ -434,14 +434,14 @@ const ListGatewayConfigsUI = ({ onBack, onExit, }) => {
434
434
  }
435
435
  // Loading state
436
436
  if (loading && configs.length === 0) {
437
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Gateway Configs", active: true }] }), _jsx(SpinnerComponent, { message: "Loading gateway configs..." })] }));
437
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "AI Gateway Configs", active: true }] }), _jsx(SpinnerComponent, { message: "Loading AI gateway configs..." })] }));
438
438
  }
439
439
  // Error state
440
440
  if (error) {
441
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Gateway Configs", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list gateway configs", error: error })] }));
441
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "AI Gateway Configs", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list AI gateway configs", error: error })] }));
442
442
  }
443
443
  // Main list view
444
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Gateway Configs", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search gateway configs..." }), !showPopup && (_jsx(Table, { data: configs, keyExtractor: (config) => config.id, selectedIndex: selectedIndex, title: `gateway_configs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No gateway configs found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedConfigItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedConfigItem, operations: operations.map((op) => ({
444
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "AI Gateway Configs", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search AI gateway configs..." }), !showPopup && (_jsx(Table, { data: configs, keyExtractor: (config) => config.id, selectedIndex: selectedIndex, title: `gateway_configs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No AI gateway configs found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedConfigItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedConfigItem, operations: operations.map((op) => ({
445
445
  key: op.key,
446
446
  label: op.label,
447
447
  color: op.color,