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:
104
src/commands/search.rs
Normal file
104
src/commands/search.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::models::Secret;
|
||||
|
||||
pub async fn run(
|
||||
pool: &PgPool,
|
||||
namespace: Option<&str>,
|
||||
kind: Option<&str>,
|
||||
tag: Option<&str>,
|
||||
query: Option<&str>,
|
||||
show_secrets: bool,
|
||||
) -> Result<()> {
|
||||
let mut conditions: Vec<String> = Vec::new();
|
||||
let mut idx: i32 = 1;
|
||||
|
||||
if namespace.is_some() {
|
||||
conditions.push(format!("namespace = ${}", idx));
|
||||
idx += 1;
|
||||
}
|
||||
if kind.is_some() {
|
||||
conditions.push(format!("kind = ${}", idx));
|
||||
idx += 1;
|
||||
}
|
||||
if tag.is_some() {
|
||||
conditions.push(format!("tags @> ARRAY[${}]", idx));
|
||||
idx += 1;
|
||||
}
|
||||
if query.is_some() {
|
||||
conditions.push(format!(
|
||||
"(name ILIKE ${i} OR namespace ILIKE ${i} OR kind ILIKE ${i} OR metadata::text ILIKE ${i} OR EXISTS (SELECT 1 FROM unnest(tags) t WHERE t ILIKE ${i}))",
|
||||
i = idx
|
||||
));
|
||||
}
|
||||
|
||||
let where_clause = if conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("WHERE {}", conditions.join(" AND "))
|
||||
};
|
||||
|
||||
let sql = format!(
|
||||
"SELECT * FROM secrets {} ORDER BY namespace, kind, name",
|
||||
where_clause
|
||||
);
|
||||
|
||||
let mut q = sqlx::query_as::<_, Secret>(&sql);
|
||||
if let Some(v) = namespace {
|
||||
q = q.bind(v);
|
||||
}
|
||||
if let Some(v) = kind {
|
||||
q = q.bind(v);
|
||||
}
|
||||
if let Some(v) = tag {
|
||||
q = q.bind(v);
|
||||
}
|
||||
if let Some(v) = query {
|
||||
q = q.bind(format!("%{}%", v));
|
||||
}
|
||||
|
||||
let rows = q.fetch_all(pool).await?;
|
||||
|
||||
if rows.is_empty() {
|
||||
println!("No records found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for row in &rows {
|
||||
println!(
|
||||
"[{}/{}] {}",
|
||||
row.namespace, row.kind, row.name,
|
||||
);
|
||||
println!(" id: {}", row.id);
|
||||
|
||||
if !row.tags.is_empty() {
|
||||
println!(" tags: [{}]", row.tags.join(", "));
|
||||
}
|
||||
|
||||
let meta_obj = row.metadata.as_object();
|
||||
if let Some(m) = meta_obj {
|
||||
if !m.is_empty() {
|
||||
println!(" metadata: {}", serde_json::to_string_pretty(&row.metadata)?);
|
||||
}
|
||||
}
|
||||
|
||||
if show_secrets {
|
||||
println!(" secrets: {}", serde_json::to_string_pretty(&row.encrypted)?);
|
||||
} else {
|
||||
let keys: Vec<String> = row
|
||||
.encrypted
|
||||
.as_object()
|
||||
.map(|m| m.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
if !keys.is_empty() {
|
||||
println!(" secrets: [{}] (--show-secrets to reveal)", keys.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
println!(" created: {}", row.created_at.format("%Y-%m-%d %H:%M:%S UTC"));
|
||||
println!();
|
||||
}
|
||||
println!("{} record(s) found.", rows.len());
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user