feat: secrets CLI MVP — add/search/delete with PostgreSQL JSONB
Some checks failed
Secrets CLI - Build & Release / 检查版本 (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Failing after 41s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Failing after 55s
Secrets CLI - Build & Release / 发送通知 (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Some checks failed
Secrets CLI - Build & Release / 检查版本 (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Failing after 41s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Failing after 55s
Secrets CLI - Build & Release / 发送通知 (push) Has been cancelled
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
- Single `secrets` table with namespace/kind/name/tags/metadata/encrypted - Auto-migrate on startup using uuidv7() primary keys and GIN indexes - CLI commands: add (upsert, @file support), search (full-text + tags), delete - Multi-platform Gitea Actions: debian (x86_64-musl), darwin-arm64, windows - continue-on-error + timeout-minutes=30 for offline runner tolerance - VS Code tasks.json for local build/test/seed - AGENTS.md for AI context Made-with: Cursor
This commit is contained in:
132
src/main.rs
Normal file
132
src/main.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
mod commands;
|
||||
mod db;
|
||||
mod models;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use dotenvy::dotenv;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "secrets", version, about = "Secrets & config manager backed by PostgreSQL")]
|
||||
struct Cli {
|
||||
/// Database URL (or set DATABASE_URL env var)
|
||||
#[arg(long, env = "DATABASE_URL", global = true, default_value = "")]
|
||||
db_url: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Add or update a record (upsert)
|
||||
Add {
|
||||
/// Namespace (e.g. refining, ricnsmart)
|
||||
#[arg(short, long)]
|
||||
namespace: String,
|
||||
/// Kind of record (server, service, key, ...)
|
||||
#[arg(long)]
|
||||
kind: String,
|
||||
/// Human-readable name
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// Tags for categorization (repeatable)
|
||||
#[arg(long = "tag")]
|
||||
tags: Vec<String>,
|
||||
/// Plaintext metadata entry: key=value (repeatable, key=@file reads from file)
|
||||
#[arg(long = "meta", short = 'm')]
|
||||
meta: Vec<String>,
|
||||
/// Secret entry: key=value (repeatable, key=@file reads from file)
|
||||
#[arg(long = "secret", short = 's')]
|
||||
secrets: Vec<String>,
|
||||
},
|
||||
|
||||
/// Search records
|
||||
Search {
|
||||
/// Filter by namespace
|
||||
#[arg(short, long)]
|
||||
namespace: Option<String>,
|
||||
/// Filter by kind
|
||||
#[arg(long)]
|
||||
kind: Option<String>,
|
||||
/// Filter by tag
|
||||
#[arg(long)]
|
||||
tag: Option<String>,
|
||||
/// Search by keyword (matches name, namespace, kind)
|
||||
#[arg(short, long)]
|
||||
query: Option<String>,
|
||||
/// Reveal encrypted secret values
|
||||
#[arg(long)]
|
||||
show_secrets: bool,
|
||||
},
|
||||
|
||||
/// Delete a record
|
||||
Delete {
|
||||
/// Namespace
|
||||
#[arg(short, long)]
|
||||
namespace: String,
|
||||
/// Kind
|
||||
#[arg(long)]
|
||||
kind: String,
|
||||
/// Name
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenv().ok();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
let db_url = if cli.db_url.is_empty() {
|
||||
std::env::var("DATABASE_URL").map_err(|_| {
|
||||
anyhow::anyhow!("DATABASE_URL not set. Use --db-url or set DATABASE_URL env var.")
|
||||
})?
|
||||
} else {
|
||||
cli.db_url.clone()
|
||||
};
|
||||
|
||||
let pool = db::create_pool(&db_url).await?;
|
||||
db::migrate(&pool).await?;
|
||||
|
||||
match &cli.command {
|
||||
Commands::Add {
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
tags,
|
||||
meta,
|
||||
secrets,
|
||||
} => {
|
||||
commands::add::run(&pool, namespace, kind, name, tags, meta, secrets).await?;
|
||||
}
|
||||
Commands::Search {
|
||||
namespace,
|
||||
kind,
|
||||
tag,
|
||||
query,
|
||||
show_secrets,
|
||||
} => {
|
||||
commands::search::run(
|
||||
&pool,
|
||||
namespace.as_deref(),
|
||||
kind.as_deref(),
|
||||
tag.as_deref(),
|
||||
query.as_deref(),
|
||||
*show_secrets,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Commands::Delete {
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
} => {
|
||||
commands::delete::run(&pool, namespace, kind, name).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user