feat: AI 优先的 search 增强与结构化输出 (v0.4.0)
Some checks failed
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 57s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Successful in 33s
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 44s
Secrets CLI - Build & Release / 发布草稿 Release (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Some checks failed
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 57s
Secrets CLI - Build & Release / Build (aarch64-apple-darwin) (push) Successful in 33s
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 44s
Secrets CLI - Build & Release / 发布草稿 Release (push) Successful in 2s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
- search: 新增 --name、-f/--field、-o/--output、--summary、--limit、--offset、--sort - search: 非 TTY 自动输出 json-compact,便于 AI 解析 - search: -f secret.* 自动解锁 secrets - add: 支持 -o json/json-compact 输出 - add: 重构为 AddArgs 结构体 - 全局: 各子命令 after_help 补充典型值示例 - output.rs: OutputMode 枚举 + TTY 检测 - 文档: README/AGENTS 面向 AI 的用法,连接串改为 <host>:<port> Made-with: Cursor
This commit is contained in:
234
src/main.rs
234
src/main.rs
@@ -3,16 +3,31 @@ mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod models;
|
||||
mod output;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use output::resolve_output_mode;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "secrets",
|
||||
version,
|
||||
about = "Secrets & config manager backed by PostgreSQL"
|
||||
about = "Secrets & config manager backed by PostgreSQL — optimised for AI agents",
|
||||
after_help = "QUICK START (AI agents):
|
||||
# Discover what namespaces / kinds exist
|
||||
secrets search --summary --limit 20
|
||||
|
||||
# Precise lookup (JSON output for easy parsing)
|
||||
secrets search -n refining --kind service --name gitea -o json --show-secrets
|
||||
|
||||
# Extract a single field value directly
|
||||
secrets search -n refining --kind service --name gitea -f secret.token
|
||||
|
||||
# Pipe-friendly (non-TTY defaults to json-compact automatically)
|
||||
secrets search -n refining --kind service | jq '.[].name'"
|
||||
)]
|
||||
struct Cli {
|
||||
/// Database URL, overrides saved config (one-time override)
|
||||
@@ -29,72 +44,181 @@ struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Add or update a record (upsert)
|
||||
/// Add or update a record (upsert). Use -m for plaintext metadata, -s for secrets.
|
||||
#[command(after_help = "EXAMPLES:
|
||||
# Add a server
|
||||
secrets add -n refining --kind server --name my-server \\
|
||||
--tag aliyun --tag shanghai \\
|
||||
-m ip=47.117.131.22 -m desc=\"Aliyun Shanghai ECS\" \\
|
||||
-s username=root -s ssh_key=@./keys/server.pem
|
||||
|
||||
# Add a service credential
|
||||
secrets add -n refining --kind service --name gitea \\
|
||||
--tag gitea \\
|
||||
-m url=https://gitea.refining.dev -m default_org=refining \\
|
||||
-s token=<token>
|
||||
|
||||
# Add with token read from a file
|
||||
secrets add -n ricnsmart --kind service --name mqtt \\
|
||||
-m host=mqtt.ricnsmart.com -m port=1883 \\
|
||||
-s password=@./mqtt_password.txt")]
|
||||
Add {
|
||||
/// Namespace (e.g. refining, ricnsmart)
|
||||
/// Namespace, e.g. refining, ricnsmart
|
||||
#[arg(short, long)]
|
||||
namespace: String,
|
||||
/// Kind of record (server, service, key, ...)
|
||||
/// Kind of record: server, service, key, ...
|
||||
#[arg(long)]
|
||||
kind: String,
|
||||
/// Human-readable name
|
||||
/// Human-readable unique name, e.g. gitea, i-uf63f2uookgs5uxmrdyc
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// Tags for categorization (repeatable)
|
||||
/// Tag for categorization (repeatable), e.g. --tag aliyun --tag hongkong
|
||||
#[arg(long = "tag")]
|
||||
tags: Vec<String>,
|
||||
/// Plaintext metadata entry: key=value (repeatable, key=@file reads from file)
|
||||
/// Plaintext metadata: key=value (repeatable; value=@file reads from file)
|
||||
#[arg(long = "meta", short = 'm')]
|
||||
meta: Vec<String>,
|
||||
/// Secret entry: key=value (repeatable, key=@file reads from file)
|
||||
/// Secret entry: key=value (repeatable; value=@file reads from file)
|
||||
#[arg(long = "secret", short = 's')]
|
||||
secrets: Vec<String>,
|
||||
/// Output format: text (default on TTY), json, json-compact, env
|
||||
#[arg(short, long = "output")]
|
||||
output: Option<String>,
|
||||
},
|
||||
|
||||
/// Search records
|
||||
/// Search / read records. This is the primary read command for AI agents.
|
||||
///
|
||||
/// Supports fuzzy search (-q), exact lookup (--name), field extraction (-f),
|
||||
/// summary view (--summary), pagination (--limit / --offset), and structured
|
||||
/// output (-o json / json-compact / env). When stdout is not a TTY, output
|
||||
/// defaults to json-compact automatically.
|
||||
#[command(after_help = "EXAMPLES:
|
||||
# Discover all records (summary, safe default limit)
|
||||
secrets search --summary --limit 20
|
||||
|
||||
# Filter by namespace and kind
|
||||
secrets search -n refining --kind service
|
||||
|
||||
# Exact lookup — returns 0 or 1 record
|
||||
secrets search -n refining --kind service --name gitea
|
||||
|
||||
# Fuzzy keyword search (matches name, namespace, kind, tags, metadata)
|
||||
secrets search -q mqtt
|
||||
|
||||
# Extract a single field value (implies --show-secrets for secret.*)
|
||||
secrets search -n refining --kind service --name gitea -f secret.token
|
||||
secrets search -n refining --kind service --name gitea -f metadata.url
|
||||
|
||||
# Multiple fields at once
|
||||
secrets search -n refining --kind service --name gitea \\
|
||||
-f metadata.url -f metadata.default_org -f secret.token
|
||||
|
||||
# Full JSON output with secrets revealed (ideal for AI parsing)
|
||||
secrets search -n refining --kind service --name gitea -o json --show-secrets
|
||||
|
||||
# Export as env vars (source-able; single record only)
|
||||
secrets search -n refining --kind service --name gitea -o env --show-secrets
|
||||
|
||||
# Paginate large result sets
|
||||
secrets search -n refining --summary --limit 10 --offset 0
|
||||
secrets search -n refining --summary --limit 10 --offset 10
|
||||
|
||||
# Sort by most recently updated
|
||||
secrets search --sort updated --limit 5 --summary
|
||||
|
||||
# Non-TTY / pipe: output is json-compact by default
|
||||
secrets search -n refining --kind service | jq '.[].name'
|
||||
secrets search -n refining --kind service --name gitea --show-secrets | jq '.secrets.token'")]
|
||||
Search {
|
||||
/// Filter by namespace
|
||||
/// Filter by namespace, e.g. refining, ricnsmart
|
||||
#[arg(short, long)]
|
||||
namespace: Option<String>,
|
||||
/// Filter by kind
|
||||
/// Filter by kind, e.g. server, service
|
||||
#[arg(long)]
|
||||
kind: Option<String>,
|
||||
/// Filter by tag
|
||||
/// Exact name filter, e.g. gitea, i-uf63f2uookgs5uxmrdyc
|
||||
#[arg(long)]
|
||||
name: Option<String>,
|
||||
/// Filter by tag, e.g. --tag aliyun
|
||||
#[arg(long)]
|
||||
tag: Option<String>,
|
||||
/// Search by keyword (matches name, namespace, kind)
|
||||
/// Fuzzy keyword (matches name, namespace, kind, tags, metadata text)
|
||||
#[arg(short, long)]
|
||||
query: Option<String>,
|
||||
/// Reveal encrypted secret values
|
||||
/// Reveal encrypted secret values in output
|
||||
#[arg(long)]
|
||||
show_secrets: bool,
|
||||
/// Extract field value(s) directly: metadata.<key> or secret.<key> (repeatable)
|
||||
#[arg(short = 'f', long = "field")]
|
||||
fields: Vec<String>,
|
||||
/// Return lightweight summary only (namespace, kind, name, tags, desc, updated_at)
|
||||
#[arg(long)]
|
||||
summary: bool,
|
||||
/// Maximum number of records to return [default: 50]
|
||||
#[arg(long, default_value = "50")]
|
||||
limit: u32,
|
||||
/// Skip this many records (for pagination)
|
||||
#[arg(long, default_value = "0")]
|
||||
offset: u32,
|
||||
/// Sort order: name (default), updated, created
|
||||
#[arg(long, default_value = "name")]
|
||||
sort: String,
|
||||
/// Output format: text (default on TTY), json, json-compact, env
|
||||
#[arg(short, long = "output")]
|
||||
output: Option<String>,
|
||||
},
|
||||
|
||||
/// Delete a record
|
||||
/// Delete a record permanently. Requires exact namespace + kind + name.
|
||||
#[command(after_help = "EXAMPLES:
|
||||
# Delete a service credential
|
||||
secrets delete -n refining --kind service --name legacy-mqtt
|
||||
|
||||
# Delete a server record
|
||||
secrets delete -n ricnsmart --kind server --name i-old-server-id")]
|
||||
Delete {
|
||||
/// Namespace
|
||||
/// Namespace, e.g. refining
|
||||
#[arg(short, long)]
|
||||
namespace: String,
|
||||
/// Kind
|
||||
/// Kind, e.g. server, service
|
||||
#[arg(long)]
|
||||
kind: String,
|
||||
/// Name
|
||||
/// Exact name of the record to delete
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Incrementally update an existing record (merge semantics)
|
||||
/// Incrementally update an existing record (merge semantics; record must exist).
|
||||
///
|
||||
/// Only the fields you pass are changed — everything else is preserved.
|
||||
/// Use --add-tag / --remove-tag to modify tags without touching other fields.
|
||||
#[command(after_help = "EXAMPLES:
|
||||
# Update a single metadata field (all other fields unchanged)
|
||||
secrets update -n refining --kind server --name my-server -m ip=10.0.0.1
|
||||
|
||||
# Rotate a secret token
|
||||
secrets update -n refining --kind service --name gitea -s token=<new-token>
|
||||
|
||||
# Add a tag and rotate password at the same time
|
||||
secrets update -n refining --kind service --name gitea \\
|
||||
--add-tag production -s token=<new-token>
|
||||
|
||||
# Remove a deprecated metadata field and a stale secret key
|
||||
secrets update -n refining --kind service --name mqtt \\
|
||||
--remove-meta old_port --remove-secret old_password
|
||||
|
||||
# Remove a tag
|
||||
secrets update -n refining --kind service --name gitea --remove-tag staging")]
|
||||
Update {
|
||||
/// Namespace (e.g. refining, ricnsmart)
|
||||
/// Namespace, e.g. refining, ricnsmart
|
||||
#[arg(short, long)]
|
||||
namespace: String,
|
||||
/// Kind of record (server, service, key, ...)
|
||||
/// Kind of record: server, service, key, ...
|
||||
#[arg(long)]
|
||||
kind: String,
|
||||
/// Human-readable name
|
||||
/// Human-readable unique name
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
/// Add a tag (repeatable)
|
||||
/// Add a tag (repeatable; does not affect existing tags)
|
||||
#[arg(long = "add-tag")]
|
||||
add_tags: Vec<String>,
|
||||
/// Remove a tag (repeatable)
|
||||
@@ -103,18 +227,27 @@ enum Commands {
|
||||
/// Set or overwrite a metadata field: key=value (repeatable, @file supported)
|
||||
#[arg(long = "meta", short = 'm')]
|
||||
meta: Vec<String>,
|
||||
/// Remove a metadata field by key (repeatable)
|
||||
/// Delete a metadata field by key (repeatable)
|
||||
#[arg(long = "remove-meta")]
|
||||
remove_meta: Vec<String>,
|
||||
/// Set or overwrite a secret field: key=value (repeatable, @file supported)
|
||||
#[arg(long = "secret", short = 's')]
|
||||
secrets: Vec<String>,
|
||||
/// Remove a secret field by key (repeatable)
|
||||
/// Delete a secret field by key (repeatable)
|
||||
#[arg(long = "remove-secret")]
|
||||
remove_secrets: Vec<String>,
|
||||
},
|
||||
|
||||
/// Manage CLI configuration (database connection, etc.)
|
||||
#[command(after_help = "EXAMPLES:
|
||||
# Configure the database URL (run once per device; persisted to config file)
|
||||
secrets config set-db \"postgres://postgres:<password>@<host>:<port>/secrets\"
|
||||
|
||||
# Show current config (password is masked)
|
||||
secrets config show
|
||||
|
||||
# Print path to the config file
|
||||
secrets config path")]
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
@@ -125,12 +258,12 @@ enum Commands {
|
||||
enum ConfigAction {
|
||||
/// Save database URL to config file (~/.config/secrets/config.toml)
|
||||
SetDb {
|
||||
/// PostgreSQL connection string
|
||||
/// PostgreSQL connection string, e.g. postgres://user:pass@<host>:<port>/dbname
|
||||
url: String,
|
||||
},
|
||||
/// Show current configuration
|
||||
/// Show current configuration (password masked)
|
||||
Show,
|
||||
/// Print path to config file
|
||||
/// Print path to the config file
|
||||
Path,
|
||||
}
|
||||
|
||||
@@ -172,26 +305,59 @@ async fn main() -> Result<()> {
|
||||
tags,
|
||||
meta,
|
||||
secrets,
|
||||
output,
|
||||
} => {
|
||||
let _span =
|
||||
tracing::info_span!("cmd", command = "add", %namespace, %kind, %name).entered();
|
||||
commands::add::run(&pool, namespace, kind, name, tags, meta, secrets).await?;
|
||||
let out = resolve_output_mode(output.as_deref())?;
|
||||
commands::add::run(
|
||||
&pool,
|
||||
commands::add::AddArgs {
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
tags,
|
||||
meta_entries: meta,
|
||||
secret_entries: secrets,
|
||||
output: out,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Commands::Search {
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
tag,
|
||||
query,
|
||||
show_secrets,
|
||||
fields,
|
||||
summary,
|
||||
limit,
|
||||
offset,
|
||||
sort,
|
||||
output,
|
||||
} => {
|
||||
let _span = tracing::info_span!("cmd", command = "search").entered();
|
||||
// -f implies --show-secrets when any field path starts with "secret"
|
||||
let show = *show_secrets || fields.iter().any(|f| f.starts_with("secret"));
|
||||
let out = resolve_output_mode(output.as_deref())?;
|
||||
commands::search::run(
|
||||
&pool,
|
||||
namespace.as_deref(),
|
||||
kind.as_deref(),
|
||||
tag.as_deref(),
|
||||
query.as_deref(),
|
||||
*show_secrets,
|
||||
commands::search::SearchArgs {
|
||||
namespace: namespace.as_deref(),
|
||||
kind: kind.as_deref(),
|
||||
name: name.as_deref(),
|
||||
tag: tag.as_deref(),
|
||||
query: query.as_deref(),
|
||||
show_secrets: show,
|
||||
fields,
|
||||
summary: *summary,
|
||||
limit: *limit,
|
||||
offset: *offset,
|
||||
sort,
|
||||
output: out,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user