@runloop/rl-cli 1.4.1 → 1.5.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
@@ -140,6 +140,16 @@ rli network-policy create # Create a new network policy
140
140
  rli network-policy delete <id> # Delete a network policy
141
141
  ```
142
142
 
143
+ ### Secret Commands (alias: `s`)
144
+
145
+ ```bash
146
+ rli secret create <name> # Create a new secret. Value can be pip...
147
+ rli secret list # List all secrets
148
+ rli secret get <name> # Get secret metadata by name
149
+ rli secret update <name> # Update a secret value (value from std...
150
+ rli secret delete <name> # Delete a secret
151
+ ```
152
+
143
153
  ### Mcp Commands
144
154
 
145
155
  ```bash
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Create secret command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ import { getSecretValue } from "../../utils/stdin.js";
7
+ export async function createSecret(name, options = {}) {
8
+ try {
9
+ // Get secret value from stdin (piped) or interactive prompt
10
+ const value = await getSecretValue();
11
+ if (!value) {
12
+ outputError("Secret value cannot be empty", new Error("Empty value"));
13
+ }
14
+ const client = getClient();
15
+ const secret = await client.secrets.create({ name, value });
16
+ // Default: just output the ID for easy scripting
17
+ if (!options.output || options.output === "text") {
18
+ console.log(secret.id);
19
+ }
20
+ else {
21
+ output(secret, { format: options.output, defaultFormat: "json" });
22
+ }
23
+ }
24
+ catch (error) {
25
+ outputError("Failed to create secret", error);
26
+ }
27
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Delete secret command
3
+ */
4
+ import * as readline from "readline";
5
+ import { getClient } from "../../utils/client.js";
6
+ import { output } from "../../utils/output.js";
7
+ /**
8
+ * Prompt for confirmation
9
+ */
10
+ async function confirm(message) {
11
+ return new Promise((resolve) => {
12
+ const rl = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+ rl.question(`${message} [y/N] `, (answer) => {
17
+ rl.close();
18
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
19
+ });
20
+ });
21
+ }
22
+ export async function deleteSecret(name, options = {}) {
23
+ try {
24
+ const client = getClient();
25
+ // Confirm deletion unless --yes flag is passed
26
+ if (!options.yes) {
27
+ const confirmed = await confirm(`Are you sure you want to delete secret "${name}"?`);
28
+ if (!confirmed) {
29
+ console.log("Aborted.");
30
+ return;
31
+ }
32
+ }
33
+ // Delete by name
34
+ const secret = await client.secrets.delete(name);
35
+ // Default: show confirmation message
36
+ if (!options.output || options.output === "text") {
37
+ console.log(`Deleted secret "${name}" (${secret.id})`);
38
+ }
39
+ else {
40
+ output({ id: secret.id, name, status: "deleted" }, { format: options.output, defaultFormat: "json" });
41
+ }
42
+ }
43
+ catch (error) {
44
+ const errorMessage = error instanceof Error ? error.message : String(error);
45
+ if (errorMessage.includes("404") || errorMessage.includes("not found")) {
46
+ console.error(`Error: Secret "${name}" not found`);
47
+ }
48
+ else {
49
+ console.error(`Error: Failed to delete secret`);
50
+ console.error(` ${errorMessage}`);
51
+ }
52
+ process.exit(1);
53
+ }
54
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Get secret metadata command
3
+ *
4
+ * Note: The API doesn't have a direct "get by name" endpoint,
5
+ * so we list all secrets and filter by name.
6
+ */
7
+ import { getClient } from "../../utils/client.js";
8
+ import { output, outputError } from "../../utils/output.js";
9
+ export async function getSecret(name, options = {}) {
10
+ try {
11
+ const client = getClient();
12
+ // List all secrets and find by name
13
+ const result = await client.secrets.list({ limit: 5000 });
14
+ const secret = result.secrets?.find((s) => s.name === name);
15
+ if (!secret) {
16
+ outputError(`Secret "${name}" not found`, new Error("Secret not found"));
17
+ }
18
+ output(secret, { format: options.output, defaultFormat: "json" });
19
+ }
20
+ catch (error) {
21
+ outputError("Failed to get secret", error);
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * List secrets command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ const DEFAULT_PAGE_SIZE = 20;
7
+ export async function listSecrets(options = {}) {
8
+ try {
9
+ const client = getClient();
10
+ const limit = options.limit
11
+ ? parseInt(options.limit, 10)
12
+ : DEFAULT_PAGE_SIZE;
13
+ // Fetch secrets
14
+ const result = await client.secrets.list({ limit });
15
+ // Extract secrets array
16
+ const secrets = result.secrets || [];
17
+ // Default: output JSON for lists
18
+ output(secrets, { format: options.output, defaultFormat: "json" });
19
+ }
20
+ catch (error) {
21
+ outputError("Failed to list secrets", error);
22
+ }
23
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Update secret command
3
+ */
4
+ import { getClient } from "../../utils/client.js";
5
+ import { output, outputError } from "../../utils/output.js";
6
+ import { getSecretValue } from "../../utils/stdin.js";
7
+ export async function updateSecret(name, options = {}) {
8
+ try {
9
+ // Get new secret value from stdin (piped) or interactive prompt
10
+ const value = await getSecretValue();
11
+ if (!value) {
12
+ outputError("Secret value cannot be empty", new Error("Empty value"));
13
+ }
14
+ const client = getClient();
15
+ const secret = await client.secrets.update(name, { value });
16
+ // Default: just output the ID for easy scripting
17
+ if (!options.output || options.output === "text") {
18
+ console.log(secret.id);
19
+ }
20
+ else {
21
+ output(secret, { format: options.output, defaultFormat: "json" });
22
+ }
23
+ }
24
+ catch (error) {
25
+ outputError("Failed to update secret", error);
26
+ }
27
+ }
@@ -439,6 +439,53 @@ export function createProgram() {
439
439
  const { deleteNetworkPolicy } = await import("../commands/network-policy/delete.js");
440
440
  await deleteNetworkPolicy(id, options);
441
441
  });
442
+ // Secret commands
443
+ const secret = program
444
+ .command("secret")
445
+ .description("Manage secrets")
446
+ .alias("s");
447
+ secret
448
+ .command("create <name>")
449
+ .description("Create a new secret. Value can be piped via stdin (e.g., echo 'val' | rli secret create name) or entered interactively with masked input for security.")
450
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
451
+ .action(async (name, options) => {
452
+ const { createSecret } = await import("../commands/secret/create.js");
453
+ await createSecret(name, options);
454
+ });
455
+ secret
456
+ .command("list")
457
+ .description("List all secrets")
458
+ .option("--limit <n>", "Max results", "20")
459
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
460
+ .action(async (options) => {
461
+ const { listSecrets } = await import("../commands/secret/list.js");
462
+ await listSecrets(options);
463
+ });
464
+ secret
465
+ .command("get <name>")
466
+ .description("Get secret metadata by name")
467
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: json)")
468
+ .action(async (name, options) => {
469
+ const { getSecret } = await import("../commands/secret/get.js");
470
+ await getSecret(name, options);
471
+ });
472
+ secret
473
+ .command("update <name>")
474
+ .description("Update a secret value (value from stdin or secure prompt)")
475
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
476
+ .action(async (name, options) => {
477
+ const { updateSecret } = await import("../commands/secret/update.js");
478
+ await updateSecret(name, options);
479
+ });
480
+ secret
481
+ .command("delete <name>")
482
+ .description("Delete a secret")
483
+ .option("-y, --yes", "Skip confirmation prompt")
484
+ .option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
485
+ .action(async (name, options) => {
486
+ const { deleteSecret } = await import("../commands/secret/delete.js");
487
+ await deleteSecret(name, options);
488
+ });
442
489
  // MCP server commands
443
490
  const mcp = program
444
491
  .command("mcp")
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Utilities for reading secure input from stdin
3
+ */
4
+ /**
5
+ * Prompt for a secret value with masked input (shows * for each character)
6
+ * Only works when stdin is a TTY (interactive terminal)
7
+ */
8
+ export async function promptSecretValue(prompt = "Enter secret value: ") {
9
+ return new Promise((resolve) => {
10
+ process.stdout.write(prompt);
11
+ let value = "";
12
+ process.stdin.setRawMode(true);
13
+ process.stdin.resume();
14
+ process.stdin.setEncoding("utf8");
15
+ const onData = (char) => {
16
+ if (char === "\n" || char === "\r") {
17
+ process.stdin.setRawMode(false);
18
+ process.stdin.removeListener("data", onData);
19
+ process.stdin.pause();
20
+ process.stdout.write("\n");
21
+ resolve(value);
22
+ }
23
+ else if (char === "\u0003") {
24
+ // Ctrl+C
25
+ process.stdin.setRawMode(false);
26
+ process.stdout.write("\n");
27
+ process.exit(0);
28
+ }
29
+ else if (char === "\u007F" || char === "\b") {
30
+ // Backspace (0x7F) or Ctrl+H (0x08)
31
+ if (value.length > 0) {
32
+ value = value.slice(0, -1);
33
+ process.stdout.write("\b \b");
34
+ }
35
+ }
36
+ else if (char >= " ") {
37
+ // Only add printable characters
38
+ value += char;
39
+ process.stdout.write("*");
40
+ }
41
+ };
42
+ process.stdin.on("data", onData);
43
+ });
44
+ }
45
+ /**
46
+ * Read all data from stdin (for piped input)
47
+ */
48
+ export async function readStdin() {
49
+ const chunks = [];
50
+ for await (const chunk of process.stdin) {
51
+ chunks.push(Buffer.from(chunk));
52
+ }
53
+ return Buffer.concat(chunks).toString().trim();
54
+ }
55
+ /**
56
+ * Get a secret value from either piped stdin or interactive prompt
57
+ * Automatically detects whether input is piped or interactive
58
+ */
59
+ export async function getSecretValue(prompt = "Enter secret value: ") {
60
+ if (process.stdin.isTTY) {
61
+ return promptSecretValue(prompt);
62
+ }
63
+ else {
64
+ return readStdin();
65
+ }
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runloop/rl-cli",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Beautiful CLI for the Runloop platform",
5
5
  "type": "module",
6
6
  "bin": {