@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.
@@ -68,6 +68,45 @@ export async function getGatewayConfig(id) {
68
68
  account_id: config.account_id ?? undefined,
69
69
  };
70
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
+ }
71
110
  /**
72
111
  * Delete a gateway config
73
112
  */
@@ -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")
@@ -119,22 +121,36 @@ export function createProgram() {
119
121
  await sshDevbox(id, options);
120
122
  });
121
123
  devbox
122
- .command("scp <id> <src> <dst>")
123
- .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.")
124
134
  .option("--scp-options <options>", "Additional scp options (quoted)")
125
135
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
126
- .action(async (id, src, dst, options) => {
136
+ .action(async (src, dst, options) => {
127
137
  const { scpFiles } = await import("../commands/devbox/scp.js");
128
- await scpFiles(id, { src, dst, ...options });
138
+ await scpFiles(src, dst, options);
129
139
  });
130
140
  devbox
131
- .command("rsync <id> <src> <dst>")
132
- .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.")
133
149
  .option("--rsync-options <options>", "Additional rsync options (quoted)")
134
150
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
135
- .action(async (id, src, dst, options) => {
151
+ .action(async (src, dst, options) => {
136
152
  const { rsyncFiles } = await import("../commands/devbox/rsync.js");
137
- await rsyncFiles(id, { src, dst, ...options });
153
+ await rsyncFiles(src, dst, options);
138
154
  });
139
155
  devbox
140
156
  .command("tunnel <id> <ports>")
@@ -530,8 +546,8 @@ export function createProgram() {
530
546
  .description("Create a new gateway configuration")
531
547
  .requiredOption("--name <name>", "Gateway config name (required)")
532
548
  .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)")
549
+ .option("--bearer-auth", "Use Bearer token authentication (default)")
550
+ .option("--header-auth <header>", "Use custom header authentication (specify header key name)")
535
551
  .option("--description <description>", "Description")
536
552
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
537
553
  .action(async (options) => {
@@ -551,8 +567,8 @@ export function createProgram() {
551
567
  .description("Update a gateway configuration")
552
568
  .option("--name <name>", "New name")
553
569
  .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)")
570
+ .option("--bearer-auth", "Use Bearer token authentication")
571
+ .option("--header-auth <header>", "Use custom header authentication (specify header key name)")
556
572
  .option("--description <description>", "New description")
557
573
  .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
558
574
  .action(async (id, options) => {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared validation for gateway config create/update operations.
3
+ * Used by both CLI commands and UI components.
4
+ */
5
+ /**
6
+ * Validate and sanitize gateway config fields.
7
+ *
8
+ * @param input - The fields to validate
9
+ * @param opts.requireName - Whether name is required (true for create, false for update)
10
+ * @param opts.requireEndpoint - Whether endpoint is required (true for create, false for update)
11
+ */
12
+ export function validateGatewayConfig(input, opts = {}) {
13
+ const errors = [];
14
+ const name = input.name?.trim();
15
+ const endpoint = input.endpoint?.trim();
16
+ const authType = input.authType?.toLowerCase();
17
+ const authKey = input.authKey?.trim();
18
+ // Name validation
19
+ if (opts.requireName && !name) {
20
+ errors.push("Name is required");
21
+ }
22
+ // Endpoint validation
23
+ if (opts.requireEndpoint && !endpoint) {
24
+ errors.push("Endpoint URL is required");
25
+ }
26
+ if (endpoint) {
27
+ if (!endpoint.startsWith("https://") && !endpoint.startsWith("http://")) {
28
+ errors.push("Endpoint must be a valid URL starting with https:// or http://");
29
+ }
30
+ // Basic URL structure validation
31
+ try {
32
+ new URL(endpoint);
33
+ }
34
+ catch {
35
+ errors.push("Endpoint is not a valid URL");
36
+ }
37
+ }
38
+ // Auth validation
39
+ if (authType && authType !== "bearer" && authType !== "header") {
40
+ errors.push('Auth type must be either "bearer" or "header"');
41
+ }
42
+ if (authType === "header" && !authKey) {
43
+ errors.push("Auth header key is required when using header authentication");
44
+ }
45
+ if (errors.length > 0) {
46
+ return { valid: false, errors };
47
+ }
48
+ return {
49
+ valid: true,
50
+ errors: [],
51
+ sanitized: {
52
+ name,
53
+ endpoint,
54
+ authType,
55
+ authKey,
56
+ },
57
+ };
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runloop/rl-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Beautiful CLI for the Runloop platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -103,6 +103,7 @@
103
103
  "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
104
104
  "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage",
105
105
  "test:components": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.components.config.js --coverage --forceExit",
106
+ "test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.e2e.config.js --forceExit",
106
107
  "docs:commands": "pnpm run build && node scripts/generate-command-docs.js"
107
108
  }
108
109
  }