feat: initial release — Infisical secret CLI with local caching
小橘 🍊(NEKO Team)
This commit is contained in:
commit
271de19d05
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
86
README.md
Normal file
86
README.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# @oc-forge/secret
|
||||||
|
|
||||||
|
🔐 Infisical secret manager CLI with local caching.
|
||||||
|
|
||||||
|
Manage secrets from [Infisical](https://infisical.com/) with a simple CLI. Caches secrets locally (24h TTL) to minimize API calls.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires Bun runtime
|
||||||
|
npm install -g @oc-forge/secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Create a config file at `~/.config/oc-secret/config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"clientId": "<your-infisical-client-id>",
|
||||||
|
"clientSecret": "<your-infisical-client-secret>",
|
||||||
|
"projectId": "<your-infisical-project-id>",
|
||||||
|
"env": "dev"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export INFISICAL_CLIENT_ID=xxx
|
||||||
|
export INFISICAL_CLIENT_SECRET=xxx
|
||||||
|
export INFISICAL_PROJECT_ID=xxx
|
||||||
|
export INFISICAL_ENV=dev # optional, defaults to "dev"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get a secret (cache-first)
|
||||||
|
secret get MY_API_KEY
|
||||||
|
|
||||||
|
# Get a secret (skip cache)
|
||||||
|
secret get MY_API_KEY --fresh
|
||||||
|
|
||||||
|
# Set/update a secret
|
||||||
|
secret set MY_API_KEY "new-value"
|
||||||
|
|
||||||
|
# List all secret keys
|
||||||
|
secret list
|
||||||
|
|
||||||
|
# List with values
|
||||||
|
secret list --show
|
||||||
|
|
||||||
|
# Sync all secrets to local cache
|
||||||
|
secret sync
|
||||||
|
|
||||||
|
# Run a command with all secrets as env vars
|
||||||
|
secret exec -- node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Cache-first**: `secret get` checks local cache (`~/.config/oc-secret/cache.json`) before hitting the API
|
||||||
|
2. **24h TTL**: Cache entries expire after 24 hours (configurable via `ttlMs` in config)
|
||||||
|
3. **Upsert**: `secret set` creates or updates the secret on Infisical and updates local cache
|
||||||
|
4. **Exec**: `secret exec` injects all secrets as environment variables into a child process
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
- Secret values go to **stdout** (clean, no decoration)
|
||||||
|
- Status messages go to **stderr** (won't pollute `$(secret get KEY)`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Safe to use in shell substitution
|
||||||
|
TOKEN=$(secret get MY_TOKEN)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Cache file is chmod 600 (owner-only read/write)
|
||||||
|
- Credentials never leave your machine
|
||||||
|
- Universal Auth (machine identity) — no user login required
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — 小橘爪作 🐾
|
||||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@oc-forge/secret",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Infisical secret manager CLI with local caching",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"secret": "./secret.ts"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"secret.ts"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"infisical",
|
||||||
|
"secret",
|
||||||
|
"cli",
|
||||||
|
"cache"
|
||||||
|
],
|
||||||
|
"author": "小橘 🍊 <xiaoju@shazhou.work>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/oc-xiaoju/secret.git"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
508
secret.ts
Normal file
508
secret.ts
Normal file
@ -0,0 +1,508 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
// @openclaw/secret - Infisical secret management CLI with local caching
|
||||||
|
// Usage: secret <command> [args]
|
||||||
|
|
||||||
|
// ─── Colors ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const c = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
dim: "\x1b[2m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
blue: "\x1b[34m",
|
||||||
|
magenta: "\x1b[35m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
};
|
||||||
|
|
||||||
|
function ok(msg: string) {
|
||||||
|
console.log(`${c.green}✓${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function info(msg: string) {
|
||||||
|
console.log(`${c.cyan}ℹ${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function warn(msg: string) {
|
||||||
|
console.log(`${c.yellow}⚠${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
function fail(msg: string) {
|
||||||
|
console.error(`${c.red}✗${c.reset} ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Paths ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const HOME = Bun.env.HOME || "~";
|
||||||
|
const CONFIG_DIR = `${HOME}/.config/oc-secret`;
|
||||||
|
const CONFIG_PATH = `${CONFIG_DIR}/config.json`;
|
||||||
|
const CACHE_PATH = `${CONFIG_DIR}/cache.json`;
|
||||||
|
|
||||||
|
// ─── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
projectId: string;
|
||||||
|
env: string;
|
||||||
|
ttlMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
value: string;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cache {
|
||||||
|
secrets: Record<string, CacheEntry>;
|
||||||
|
lastSync: number;
|
||||||
|
ttlMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadConfig(): Promise<Config> {
|
||||||
|
let fileConfig: Partial<Config> = {};
|
||||||
|
|
||||||
|
const configFile = Bun.file(CONFIG_PATH);
|
||||||
|
if (await configFile.exists()) {
|
||||||
|
try {
|
||||||
|
fileConfig = await configFile.json();
|
||||||
|
} catch {
|
||||||
|
warn("config.json is malformed, using env vars only");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId =
|
||||||
|
Bun.env.INFISICAL_CLIENT_ID || fileConfig.clientId || "";
|
||||||
|
const clientSecret =
|
||||||
|
Bun.env.INFISICAL_CLIENT_SECRET || fileConfig.clientSecret || "";
|
||||||
|
const projectId =
|
||||||
|
Bun.env.INFISICAL_PROJECT_ID ||
|
||||||
|
fileConfig.projectId ||
|
||||||
|
"";
|
||||||
|
const env = Bun.env.INFISICAL_ENV || fileConfig.env || "dev";
|
||||||
|
const ttlMs = fileConfig.ttlMs || 86400000; // 24h
|
||||||
|
|
||||||
|
if (!clientId || !clientSecret) {
|
||||||
|
fail(
|
||||||
|
"Missing Infisical credentials. Set INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET or add them to config.json"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
fail(
|
||||||
|
"Missing Infisical project ID. Set INFISICAL_PROJECT_ID or add projectId to config.json"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { clientId, clientSecret, projectId, env, ttlMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Cache ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function loadCache(): Promise<Cache> {
|
||||||
|
const cacheFile = Bun.file(CACHE_PATH);
|
||||||
|
if (await cacheFile.exists()) {
|
||||||
|
try {
|
||||||
|
const data = await cacheFile.json();
|
||||||
|
if (data && typeof data.secrets === "object") {
|
||||||
|
return data as Cache;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
warn("Cache file corrupted, starting fresh");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { secrets: {}, lastSync: 0, ttlMs: 86400000 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCache(cache: Cache): Promise<void> {
|
||||||
|
await Bun.write(CACHE_PATH, JSON.stringify(cache, null, 2));
|
||||||
|
// Set file permissions to 600
|
||||||
|
const proc = Bun.spawn(["chmod", "600", CACHE_PATH]);
|
||||||
|
await proc.exited;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCacheValid(entry: CacheEntry, ttlMs: number): boolean {
|
||||||
|
return Date.now() - entry.updatedAt < ttlMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Infisical API ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const API_BASE = "https://app.infisical.com/api";
|
||||||
|
|
||||||
|
async function authenticate(config: Config): Promise<string> {
|
||||||
|
const res = await fetch(`${API_BASE}/v1/auth/universal-auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
clientId: config.clientId,
|
||||||
|
clientSecret: config.clientSecret,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
if (res.status === 401 || res.status === 403) {
|
||||||
|
fail("Authentication failed — check your clientId/clientSecret");
|
||||||
|
} else {
|
||||||
|
fail(`Auth request failed (${res.status}): ${body}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as { accessToken: string };
|
||||||
|
return data.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllSecrets(
|
||||||
|
token: string,
|
||||||
|
config: Config
|
||||||
|
): Promise<Array<{ secretKey: string; secretValue: string }>> {
|
||||||
|
const url = `${API_BASE}/v3/secrets/raw?environment=${encodeURIComponent(config.env)}&workspaceId=${encodeURIComponent(config.projectId)}`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
fail(`Failed to fetch secrets (${res.status}): ${body}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
secrets: Array<{ secretKey: string; secretValue: string }>;
|
||||||
|
};
|
||||||
|
return data.secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOneSecret(
|
||||||
|
token: string,
|
||||||
|
config: Config,
|
||||||
|
key: string
|
||||||
|
): Promise<string> {
|
||||||
|
const url = `${API_BASE}/v3/secrets/raw/${encodeURIComponent(key)}?environment=${encodeURIComponent(config.env)}&workspaceId=${encodeURIComponent(config.projectId)}`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 404) {
|
||||||
|
fail(`Secret "${key}" not found`);
|
||||||
|
} else {
|
||||||
|
const body = await res.text();
|
||||||
|
fail(`Failed to fetch secret (${res.status}): ${body}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
secret: { secretKey: string; secretValue: string };
|
||||||
|
};
|
||||||
|
return data.secret.secretValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertSecret(
|
||||||
|
token: string,
|
||||||
|
config: Config,
|
||||||
|
key: string,
|
||||||
|
value: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Try PATCH first (update existing)
|
||||||
|
const patchUrl = `${API_BASE}/v3/secrets/raw/${encodeURIComponent(key)}`;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
workspaceId: config.projectId,
|
||||||
|
environment: config.env,
|
||||||
|
secretValue: value,
|
||||||
|
type: "shared",
|
||||||
|
});
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchRes = await fetch(patchUrl, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (patchRes.ok) return;
|
||||||
|
|
||||||
|
// If PATCH fails (404 = doesn't exist), try POST to create
|
||||||
|
if (patchRes.status === 400 || patchRes.status === 404) {
|
||||||
|
const postRes = await fetch(`${API_BASE}/v3/secrets/raw/${encodeURIComponent(key)}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!postRes.ok) {
|
||||||
|
const errBody = await postRes.text();
|
||||||
|
fail(`Failed to create secret (${postRes.status}): ${errBody}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errBody = await patchRes.text();
|
||||||
|
fail(`Failed to update secret (${patchRes.status}): ${errBody}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Commands ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function cmdGet(key: string, fresh: boolean) {
|
||||||
|
const config = await loadConfig();
|
||||||
|
const cache = await loadCache();
|
||||||
|
|
||||||
|
// Check cache first (unless --fresh)
|
||||||
|
if (!fresh && cache.secrets[key] && isCacheValid(cache.secrets[key], config.ttlMs)) {
|
||||||
|
const val = cache.secrets[key].value;
|
||||||
|
console.log(val);
|
||||||
|
console.error(`${c.cyan}ℹ${c.reset} ${c.dim}(from cache)${c.reset}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from Infisical
|
||||||
|
const token = await authenticate(config);
|
||||||
|
const value = await fetchOneSecret(token, config, key);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cache.secrets[key] = { value, updatedAt: Date.now() };
|
||||||
|
cache.ttlMs = config.ttlMs;
|
||||||
|
await saveCache(cache);
|
||||||
|
|
||||||
|
console.log(value);
|
||||||
|
console.error(`${c.cyan}ℹ${c.reset} ${c.dim}(from Infisical)${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSet(key: string, value: string) {
|
||||||
|
const config = await loadConfig();
|
||||||
|
const token = await authenticate(config);
|
||||||
|
|
||||||
|
await upsertSecret(token, config, key, value);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
const cache = await loadCache();
|
||||||
|
cache.secrets[key] = { value, updatedAt: Date.now() };
|
||||||
|
cache.ttlMs = config.ttlMs;
|
||||||
|
await saveCache(cache);
|
||||||
|
|
||||||
|
ok(`Set ${c.bold}${key}${c.reset} ✓`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdList(showValues: boolean) {
|
||||||
|
const config = await loadConfig();
|
||||||
|
const token = await authenticate(config);
|
||||||
|
const secrets = await fetchAllSecrets(token, config);
|
||||||
|
|
||||||
|
if (secrets.length === 0) {
|
||||||
|
warn("No secrets found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by key
|
||||||
|
secrets.sort((a, b) => a.secretKey.localeCompare(b.secretKey));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\n${c.bold}${c.cyan}Secrets${c.reset} ${c.dim}(${secrets.length} total, env: ${config.env})${c.reset}\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxKeyLen = Math.max(...secrets.map((s) => s.secretKey.length));
|
||||||
|
|
||||||
|
for (const s of secrets) {
|
||||||
|
const key = s.secretKey.padEnd(maxKeyLen);
|
||||||
|
if (showValues) {
|
||||||
|
console.log(` ${c.green}${key}${c.reset} ${c.dim}=${c.reset} ${s.secretValue}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${c.green}${key}${c.reset}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdSync() {
|
||||||
|
const config = await loadConfig();
|
||||||
|
const token = await authenticate(config);
|
||||||
|
const secrets = await fetchAllSecrets(token, config);
|
||||||
|
|
||||||
|
const cache: Cache = {
|
||||||
|
secrets: {},
|
||||||
|
lastSync: Date.now(),
|
||||||
|
ttlMs: config.ttlMs,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const s of secrets) {
|
||||||
|
cache.secrets[s.secretKey] = {
|
||||||
|
value: s.secretValue,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveCache(cache);
|
||||||
|
ok(`Synced ${c.bold}${secrets.length}${c.reset} secrets to cache`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmdExec(args: string[]) {
|
||||||
|
if (args.length === 0) {
|
||||||
|
fail("Usage: secret exec -- <command> [args...]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await loadConfig();
|
||||||
|
const cache = await loadCache();
|
||||||
|
|
||||||
|
// Check if cache is fresh enough, otherwise sync
|
||||||
|
const hasValidCache =
|
||||||
|
Object.keys(cache.secrets).length > 0 &&
|
||||||
|
Date.now() - cache.lastSync < config.ttlMs;
|
||||||
|
|
||||||
|
let secrets: Record<string, string>;
|
||||||
|
|
||||||
|
if (hasValidCache) {
|
||||||
|
secrets = Object.fromEntries(
|
||||||
|
Object.entries(cache.secrets).map(([k, v]) => [k, v.value])
|
||||||
|
);
|
||||||
|
info(`${c.dim}Using cached secrets${c.reset}`);
|
||||||
|
} else {
|
||||||
|
// Fetch fresh
|
||||||
|
const token = await authenticate(config);
|
||||||
|
const fetched = await fetchAllSecrets(token, config);
|
||||||
|
|
||||||
|
secrets = Object.fromEntries(
|
||||||
|
fetched.map((s) => [s.secretKey, s.secretValue])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
const newCache: Cache = {
|
||||||
|
secrets: {},
|
||||||
|
lastSync: Date.now(),
|
||||||
|
ttlMs: config.ttlMs,
|
||||||
|
};
|
||||||
|
for (const s of fetched) {
|
||||||
|
newCache.secrets[s.secretKey] = {
|
||||||
|
value: s.secretValue,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await saveCache(newCache);
|
||||||
|
info(`${c.dim}Synced ${fetched.length} secrets${c.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge secrets into env and exec
|
||||||
|
const env = { ...Bun.env, ...secrets };
|
||||||
|
|
||||||
|
const proc = Bun.spawn(args, {
|
||||||
|
env,
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
stdin: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCode = await proc.exited;
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function printUsage() {
|
||||||
|
console.log(`
|
||||||
|
${c.bold}${c.cyan}@openclaw/secret${c.reset} — Infisical secret manager with local caching
|
||||||
|
|
||||||
|
${c.bold}Usage:${c.reset}
|
||||||
|
secret get <KEY> [--fresh] Get a secret value (cache-first)
|
||||||
|
secret set <KEY> <VALUE> Set/update a secret
|
||||||
|
secret list [--show] List all secret keys
|
||||||
|
secret sync Sync all secrets to local cache
|
||||||
|
secret exec -- <cmd> [args] Run command with secrets as env vars
|
||||||
|
|
||||||
|
${c.bold}Config:${c.reset}
|
||||||
|
${c.dim}~/.config/oc-secret/config.json${c.reset}
|
||||||
|
${c.dim}or INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET env vars${c.reset}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
printUsage();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = args[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case "get": {
|
||||||
|
const key = args[1];
|
||||||
|
if (!key) {
|
||||||
|
fail("Usage: secret get <KEY> [--fresh]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const fresh = args.includes("--fresh");
|
||||||
|
await cmdGet(key, fresh);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "set": {
|
||||||
|
const key = args[1];
|
||||||
|
const value = args[2];
|
||||||
|
if (!key || value === undefined) {
|
||||||
|
fail("Usage: secret set <KEY> <VALUE>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await cmdSet(key, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "list": {
|
||||||
|
const showValues = args.includes("--show");
|
||||||
|
await cmdList(showValues);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "sync": {
|
||||||
|
await cmdSync();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "exec": {
|
||||||
|
// Find "--" separator
|
||||||
|
const dashIdx = args.indexOf("--");
|
||||||
|
const cmdArgs = dashIdx >= 0 ? args.slice(dashIdx + 1) : args.slice(1);
|
||||||
|
await cmdExec(cmdArgs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "help":
|
||||||
|
case "--help":
|
||||||
|
case "-h": {
|
||||||
|
printUsage();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
fail(`Unknown command: ${command}`);
|
||||||
|
printUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof TypeError && String(err).includes("fetch")) {
|
||||||
|
fail("Network error — check your internet connection");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (err instanceof Error) {
|
||||||
|
fail(err.message);
|
||||||
|
} else {
|
||||||
|
fail(String(err));
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Loading…
x
Reference in New Issue
Block a user