@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 +10 -0
- package/dist/commands/secret/create.js +27 -0
- package/dist/commands/secret/delete.js +54 -0
- package/dist/commands/secret/get.js +23 -0
- package/dist/commands/secret/list.js +23 -0
- package/dist/commands/secret/update.js +27 -0
- package/dist/utils/commands.js +47 -0
- package/dist/utils/stdin.js +66 -0
- package/package.json +1 -1
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
|
+
}
|
package/dist/utils/commands.js
CHANGED
|
@@ -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
|
+
}
|