chore: local timezone in text output, search metadata-only, bump 0.7.3
Some checks failed
Secrets CLI - Build & Release / 版本 & Release (push) Successful in 3s
Secrets CLI - Build & Release / Build (x86_64-unknown-linux-musl) (push) Successful in 2m15s
Secrets CLI - Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 1m50s
Secrets CLI - Build & Release / Build (macOS aarch64 + x86_64) (push) Successful in 44s
Secrets CLI - Build & Release / Build (x86_64-pc-windows-msvc) (push) Has been cancelled
Secrets CLI - Build & Release / 发布草稿 Release (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
voson
2026-03-19 12:24:20 +08:00
parent 4ddafbe4b6
commit 5a5867adc1
10 changed files with 207 additions and 144 deletions

8
.vscode/tasks.json vendored
View File

@@ -104,9 +104,9 @@
"dependsOn": "build"
},
{
"label": "test: search with secrets revealed",
"label": "test: inject service secrets",
"type": "shell",
"command": "./target/debug/secrets search -n refining --kind service --show-secrets",
"command": "./target/debug/secrets inject -n refining --kind service --name gitea",
"dependsOn": "build"
},
{
@@ -118,7 +118,7 @@
{
"label": "test: add + delete roundtrip",
"type": "shell",
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name roundtrip-test --tag test -m foo=bar -s password=secret123 && echo '--- search ---' && ./target/debug/secrets search -n test --show-secrets && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name roundtrip-test && echo '--- verify deleted ---' && ./target/debug/secrets search -n test",
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name roundtrip-test --tag test -m foo=bar -s password=secret123 && echo '--- search metadata ---' && ./target/debug/secrets search -n test && echo '--- inject secrets ---' && ./target/debug/secrets inject -n test --kind demo --name roundtrip-test && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name roundtrip-test && echo '--- verify deleted ---' && ./target/debug/secrets search -n test",
"dependsOn": "build"
},
{
@@ -142,7 +142,7 @@
{
"label": "test: add with file secret",
"type": "shell",
"command": "echo '--- add key from file ---' && ./target/debug/secrets add -n test --kind key --name test-key --tag test -s content=@./refining/keys/Vultr && echo '--- verify ---' && ./target/debug/secrets search -n test --kind key --show-secrets && echo '--- cleanup ---' && ./target/debug/secrets delete -n test --kind key --name test-key",
"command": "echo '--- add key from file ---' && ./target/debug/secrets add -n test --kind key --name test-key --tag test -s content=@./refining/keys/Vultr && echo '--- verify metadata ---' && ./target/debug/secrets search -n test --kind key && echo '--- verify inject ---' && ./target/debug/secrets inject -n test --kind key --name test-key && echo '--- cleanup ---' && ./target/debug/secrets delete -n test --kind key --name test-key",
"dependsOn": "build"
}
]

View File

@@ -176,8 +176,8 @@ secrets init
# --name gitea | i-uf63f2uookgs5uxmrdyc | mqtt
# --tag aliyun | hongkong | production
# -q / --query mqtt | grafana | gitea (模糊匹配 name/namespace/kind/tags/metadata
# --show-secrets 不带值的 flag显示 encrypted 字段内容
# -f / --field metadata.ip | metadata.url | secret.token | secret.ssh_key
# --show-secrets 已弃用search 不再直接展示 secrets
# -f / --field metadata.ip | metadata.url | metadata.default_org
# --summary 不带值的 flag仅返回摘要name/tags/desc/updated_at
# --limit 20 | 50默认 50
# --offset 0 | 10 | 20分页偏移
@@ -193,14 +193,17 @@ secrets search --sort updated --limit 10 --summary
secrets search -n refining --kind service --name gitea
secrets search -n refining --kind server --name i-uf63f2uookgs5uxmrdyc
# 精确定位并获取完整内容(secrets
secrets search -n refining --kind service --name gitea -o json --show-secrets
# 精确定位并获取完整内容secrets 保持加密占位
secrets search -n refining --kind service --name gitea -o json
# 直接提取字段值(最短路径-f secret.* 自动解锁 secrets
secrets search -n refining --kind service --name gitea -f secret.token
# 直接提取 metadata 字段值(最短路径)
secrets search -n refining --kind service --name gitea -f metadata.url
secrets search -n refining --kind service --name gitea \
-f metadata.url -f metadata.default_org -f secret.token
-f metadata.url -f metadata.default_org
# 需要 secrets 时,改用 inject / run
secrets inject -n refining --kind service --name gitea
secrets run -n refining --kind service --name gitea -- printenv
# 模糊关键词搜索
secrets search -q mqtt
@@ -219,10 +222,9 @@ secrets search -n refining --summary --limit 10 --offset 10
# 管道 / AI 调用(非 TTY 自动 json-compact
secrets search -n refining --kind service | jq '.[].name'
secrets search -n refining --kind service --name gitea --show-secrets | jq '.secrets.token'
# 导出为 env 文件(单条记录)
secrets search -n refining --kind service --name gitea -o env --show-secrets \
# 导出 metadata 为 env 文件(单条记录)
secrets search -n refining --kind service --name gitea -o env \
> ~/.config/gitea/config.env
```

2
Cargo.lock generated
View File

@@ -1836,7 +1836,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "secrets"
version = "0.7.2"
version = "0.7.3"
dependencies = [
"aes-gcm",
"anyhow",

View File

@@ -1,6 +1,6 @@
[package]
name = "secrets"
version = "0.7.2"
version = "0.7.3"
edition = "2024"
[dependencies]

View File

@@ -54,37 +54,46 @@ secrets search --sort updated --limit 10 --summary
# 精确定位namespace + kind + name 三元组)
secrets search -n refining --kind service --name gitea
# 获取完整记录secretsJSON 格式AI 最易解析
secrets search -n refining --kind service --name gitea -o json --show-secrets
# 获取完整记录secrets 保持加密占位
secrets search -n refining --kind service --name gitea -o json
# 直接提取单个字段值(最短路径)
secrets search -n refining --kind service --name gitea -f secret.token
# 直接提取单个 metadata 字段值(最短路径)
secrets search -n refining --kind service --name gitea -f metadata.url
# 同时提取多个字段
# 同时提取多个 metadata 字段
secrets search -n refining --kind service --name gitea \
-f metadata.url -f metadata.default_org -f secret.token
-f metadata.url -f metadata.default_org
# 需要 secrets 时,改用 inject / run
secrets inject -n refining --kind service --name gitea
secrets run -n refining --kind service --name gitea -- printenv
```
`-f secret.*` 会自动解锁 secrets无需额外加 `--show-secrets`
`search` 只负责发现、定位和读取 metadata不直接展示 secrets。
### 输出格式
| 场景 | 推荐命令 |
|------|----------|
| AI 解析 / 管道处理 | `-o json``-o json-compact` |
| 写入 `.env` 文件 | `-o env --show-secrets` |
| 写入 metadata `.env` 文件 | `-o env` |
| 注入 secrets 到环境变量 | `inject` / `run` |
| 人类查看 | 默认 `text`TTY 下自动启用) |
| 非 TTY管道/重定向) | 自动 `json-compact` |
说明:`text` 输出中的时间会按当前机器本地时区显示;`json/json-compact` 继续使用 UTCRFC3339 风格)以便脚本和 AI 稳定解析。
```bash
# 管道直接 jq 解析(非 TTY 自动 json-compact
secrets search -n refining --kind service | jq '.[].name'
secrets search -n refining --kind service --name gitea --show-secrets | jq '.secrets.token'
# 导出为可 source 的 env 文件(单条记录)
secrets search -n refining --kind service --name gitea -o env --show-secrets \
# 导出 metadata 为可 source 的 env 文件(单条记录)
secrets search -n refining --kind service --name gitea -o env \
> ~/.config/gitea/config.env
# 需要 secrets 时,使用 inject / run
secrets inject -n refining --kind service --name gitea > ~/.config/gitea/secrets.env
secrets run -n refining --kind service --name gitea -- ./deploy.sh
```
## 完整命令参考
@@ -106,8 +115,8 @@ secrets search -n refining --kind service # 按 namespace + kin
secrets search -n refining --kind service --name gitea # 精确查找
secrets search -q mqtt # 关键词模糊搜索
secrets search --tag hongkong # 按 tag 过滤
secrets search -n refining --kind service --name gitea -f secret.token # 提取字段
secrets search -n refining --kind service --name gitea -o json --show-secrets # 完整 JSON
secrets search -n refining --kind service --name gitea -f metadata.url # 提取 metadata 字段
secrets search -n refining --kind service --name gitea -o json # 完整记录secrets 保持占位)
secrets search --sort updated --limit 10 --summary # 最近改动
secrets search -n refining --summary --limit 10 --offset 10 # 翻页

View File

@@ -3,7 +3,7 @@ use serde_json::{Value, json};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
use crate::output::OutputMode;
use crate::output::{OutputMode, format_local_time};
#[derive(FromRow)]
struct HistoryRow {
@@ -228,7 +228,7 @@ pub async fn list_history(
r.version,
r.action,
r.actor,
r.created_at.format("%Y-%m-%d %H:%M:%S UTC")
format_local_time(r.created_at)
);
}
println!(" (use `secrets rollback --to-version <N>` to restore)");

View File

@@ -3,7 +3,7 @@ use serde_json::Value;
use sqlx::PgPool;
use std::collections::HashMap;
use crate::commands::search::build_env_map;
use crate::commands::search::build_injected_env_map;
use crate::output::OutputMode;
pub struct InjectArgs<'a> {
@@ -48,7 +48,7 @@ pub async fn collect_env_map(
}
let mut map = HashMap::new();
for row in &rows {
let row_map = build_env_map(row, prefix, Some(master_key))?;
let row_map = build_injected_env_map(row, prefix, master_key)?;
for (k, v) in row_map {
map.insert(k, v);
}

View File

@@ -5,7 +5,7 @@ use std::collections::HashMap;
use crate::crypto;
use crate::models::Secret;
use crate::output::OutputMode;
use crate::output::{OutputMode, format_local_time};
pub struct SearchArgs<'a> {
pub namespace: Option<&'a str>,
@@ -22,7 +22,9 @@ pub struct SearchArgs<'a> {
pub output: OutputMode,
}
pub async fn run(pool: &PgPool, args: SearchArgs<'_>, master_key: Option<&[u8; 32]>) -> Result<()> {
pub async fn run(pool: &PgPool, args: SearchArgs<'_>) -> Result<()> {
validate_safe_search_args(args.show_secrets, args.fields)?;
let rows = fetch_rows_paged(
pool,
PagedFetchArgs {
@@ -40,15 +42,12 @@ pub async fn run(pool: &PgPool, args: SearchArgs<'_>, master_key: Option<&[u8; 3
// -f/--field: extract specific field values directly
if !args.fields.is_empty() {
return print_fields(&rows, args.fields, master_key);
return print_fields(&rows, args.fields);
}
match args.output {
OutputMode::Json | OutputMode::JsonCompact => {
let arr: Vec<Value> = rows
.iter()
.map(|r| to_json(r, args.show_secrets, args.summary, master_key))
.collect();
let arr: Vec<Value> = rows.iter().map(|r| to_json(r, args.summary)).collect();
let out = if args.output == OutputMode::Json {
serde_json::to_string_pretty(&arr)?
} else {
@@ -64,7 +63,7 @@ pub async fn run(pool: &PgPool, args: SearchArgs<'_>, master_key: Option<&[u8; 3
);
}
if let Some(row) = rows.first() {
let map = build_env_map(row, "", master_key)?;
let map = build_metadata_env_map(row, "");
let mut pairs: Vec<(String, String)> = map.into_iter().collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
for (k, v) in pairs {
@@ -80,7 +79,7 @@ pub async fn run(pool: &PgPool, args: SearchArgs<'_>, master_key: Option<&[u8; 3
return Ok(());
}
for row in &rows {
print_text(row, args.show_secrets, args.summary, master_key)?;
print_text(row, args.summary)?;
}
println!("{} record(s) found.", rows.len());
if rows.len() == args.limit as usize {
@@ -96,6 +95,30 @@ pub async fn run(pool: &PgPool, args: SearchArgs<'_>, master_key: Option<&[u8; 3
Ok(())
}
fn validate_safe_search_args(show_secrets: bool, fields: &[String]) -> Result<()> {
if show_secrets {
anyhow::bail!(
"`search` no longer reveals secrets. Use `secrets inject` or `secrets run` instead."
);
}
if let Some(field) = fields.iter().find(|field| is_secret_field(field)) {
anyhow::bail!(
"Field '{}' is sensitive. `search -f` only supports metadata.* fields; use `secrets inject` or `secrets run` for secrets.",
field
);
}
Ok(())
}
fn is_secret_field(field: &str) -> bool {
matches!(
field.split_once('.').map(|(section, _)| section),
Some("secret" | "secrets" | "encrypted")
)
}
/// Fetch rows with simple equality/tag filters (no pagination). Used by inject/run.
pub async fn fetch_rows(
pool: &PgPool,
@@ -218,16 +241,9 @@ async fn fetch_rows_paged(pool: &PgPool, a: PagedFetchArgs<'_>) -> Result<Vec<Se
Ok(rows)
}
/// Build a flat `KEY=VALUE` map from a record's metadata and decrypted secrets.
/// Variable names: `<PREFIX><NAME>_<FIELD>` (all uppercased, hyphens/dots → underscores).
/// If `prefix` is empty, the name segment alone is used as the prefix.
pub fn build_env_map(
row: &Secret,
prefix: &str,
master_key: Option<&[u8; 32]>,
) -> Result<HashMap<String, String>> {
fn env_prefix(row: &Secret, prefix: &str) -> String {
let name_part = row.name.to_uppercase().replace(['-', '.', ' '], "_");
let effective_prefix = if prefix.is_empty() {
if prefix.is_empty() {
name_part
} else {
format!(
@@ -235,7 +251,14 @@ pub fn build_env_map(
prefix.to_uppercase().replace(['-', '.', ' '], "_"),
name_part
)
};
}
}
/// Build a flat `KEY=VALUE` map from metadata only.
/// Variable names: `<PREFIX><NAME>_<FIELD>` (all uppercased, hyphens/dots → underscores).
/// If `prefix` is empty, the name segment alone is used as the prefix.
pub fn build_metadata_env_map(row: &Secret, prefix: &str) -> HashMap<String, String> {
let effective_prefix = env_prefix(row, prefix);
let mut map = HashMap::new();
@@ -250,9 +273,19 @@ pub fn build_env_map(
}
}
if let Some(master_key) = master_key
&& !row.encrypted.is_empty()
{
map
}
/// Build a flat `KEY=VALUE` map from metadata and decrypted secrets.
pub fn build_injected_env_map(
row: &Secret,
prefix: &str,
master_key: &[u8; 32],
) -> Result<HashMap<String, String>> {
let effective_prefix = env_prefix(row, prefix);
let mut map = build_metadata_env_map(row, prefix);
if !row.encrypted.is_empty() {
let decrypted = crypto::decrypt_json(master_key, &row.encrypted)?;
if let Some(enc) = decrypted.as_object() {
for (k, v) in enc {
@@ -284,23 +317,7 @@ fn json_value_to_env_string(v: &Value) -> String {
}
}
/// Decrypt the encrypted blob for a row. Returns an empty object on empty blobs.
fn try_decrypt(row: &Secret, master_key: Option<&[u8; 32]>) -> Result<Value> {
if row.encrypted.is_empty() {
return Ok(Value::Object(Default::default()));
}
let key = master_key.ok_or_else(|| {
anyhow::anyhow!("master key required to decrypt secrets (run `secrets init`)")
})?;
crypto::decrypt_json(key, &row.encrypted)
}
fn to_json(
row: &Secret,
show_secrets: bool,
summary: bool,
master_key: Option<&[u8; 32]>,
) -> Value {
fn to_json(row: &Secret, summary: bool) -> Value {
if summary {
let desc = row
.metadata
@@ -319,11 +336,8 @@ fn to_json(
});
}
let secrets_val = if show_secrets {
match try_decrypt(row, master_key) {
Ok(v) => v,
Err(e) => json!({"_error": e.to_string()}),
}
let secrets_val = if row.encrypted.is_empty() {
Value::Object(Default::default())
} else {
json!({"_encrypted": true})
};
@@ -342,12 +356,7 @@ fn to_json(
})
}
fn print_text(
row: &Secret,
show_secrets: bool,
summary: bool,
master_key: Option<&[u8; 32]>,
) -> Result<()> {
fn print_text(row: &Secret, summary: bool) -> Result<()> {
println!("[{}/{}] {}", row.namespace, row.kind, row.name);
if summary {
let desc = row
@@ -360,10 +369,7 @@ fn print_text(
println!(" tags: [{}]", row.tags.join(", "));
}
println!(" desc: {}", desc);
println!(
" updated: {}",
row.updated_at.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(" updated: {}", format_local_time(row.updated_at));
} else {
println!(" id: {}", row.id);
if !row.tags.is_empty() {
@@ -376,61 +382,33 @@ fn print_text(
);
}
if !row.encrypted.is_empty() {
if show_secrets {
match try_decrypt(row, master_key) {
Ok(v) => println!(" secrets: {}", serde_json::to_string_pretty(&v)?),
Err(e) => println!(" secrets: [decrypt error: {}]", e),
}
} else {
println!(" secrets: [encrypted] (--show-secrets to reveal)");
}
println!(" secrets: [encrypted] (use `secrets inject` or `secrets run`)");
}
println!(
" created: {}",
row.created_at.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(" created: {}", format_local_time(row.created_at));
}
println!();
Ok(())
}
/// Extract one or more field paths like `metadata.url` or `secret.token`.
fn print_fields(rows: &[Secret], fields: &[String], master_key: Option<&[u8; 32]>) -> Result<()> {
/// Extract one or more field paths like `metadata.url`.
fn print_fields(rows: &[Secret], fields: &[String]) -> Result<()> {
for row in rows {
let decrypted: Option<Value> = if fields
.iter()
.any(|f| f.starts_with("secret") || f.starts_with("encrypted"))
{
Some(try_decrypt(row, master_key)?)
} else {
None
};
for field in fields {
let val = extract_field(row, field, decrypted.as_ref())?;
let val = extract_field(row, field)?;
println!("{}", val);
}
}
Ok(())
}
fn extract_field(row: &Secret, field: &str, decrypted: Option<&Value>) -> Result<String> {
let (section, key) = field.split_once('.').ok_or_else(|| {
anyhow::anyhow!(
"Invalid field path '{}'. Use metadata.<key> or secret.<key>",
field
)
})?;
fn extract_field(row: &Secret, field: &str) -> Result<String> {
let (section, key) = field
.split_once('.')
.ok_or_else(|| anyhow::anyhow!("Invalid field path '{}'. Use metadata.<key>.", field))?;
let obj = match section {
"metadata" | "meta" => &row.metadata,
"secret" | "secrets" | "encrypted" => {
decrypted.ok_or_else(|| anyhow::anyhow!("secret field requires master key"))?
}
other => anyhow::bail!(
"Unknown field section '{}'. Use 'metadata' or 'secret'",
other
),
other => anyhow::bail!("Unknown field section '{}'. Use 'metadata'.", other),
};
obj.get(key)
@@ -449,3 +427,70 @@ fn extract_field(row: &Secret, field: &str, decrypted: Option<&Value>) -> Result
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use serde_json::json;
use uuid::Uuid;
fn sample_secret() -> Secret {
let key = [0x42u8; 32];
let encrypted = crypto::encrypt_json(&key, &json!({"token": "abc123"})).unwrap();
Secret {
id: Uuid::nil(),
namespace: "refining".to_string(),
kind: "service".to_string(),
name: "gitea.main".to_string(),
tags: vec!["prod".to_string()],
metadata: json!({"url": "https://gitea.refining.dev", "enabled": true}),
encrypted,
version: 1,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn rejects_show_secrets_flag() {
let err = validate_safe_search_args(true, &[]).unwrap_err();
assert!(err.to_string().contains("no longer reveals secrets"));
}
#[test]
fn rejects_secret_field_extraction() {
let fields = vec!["secret.token".to_string()];
let err = validate_safe_search_args(false, &fields).unwrap_err();
assert!(err.to_string().contains("sensitive"));
}
#[test]
fn metadata_env_map_excludes_secret_values() {
let row = sample_secret();
let map = build_metadata_env_map(&row, "");
assert_eq!(
map.get("GITEA_MAIN_URL").map(String::as_str),
Some("https://gitea.refining.dev")
);
assert_eq!(
map.get("GITEA_MAIN_ENABLED").map(String::as_str),
Some("true")
);
assert!(!map.contains_key("GITEA_MAIN_TOKEN"));
}
#[test]
fn injected_env_map_includes_secret_values() {
let row = sample_secret();
let key = [0x42u8; 32];
let map = build_injected_env_map(&row, "", &key).unwrap();
assert_eq!(
map.get("GITEA_MAIN_TOKEN").map(String::as_str),
Some("abc123")
);
}
}

View File

@@ -28,13 +28,16 @@ use output::resolve_output_mode;
secrets search --summary --limit 20
# Precise lookup (JSON output for easy parsing)
secrets search -n refining --kind service --name gitea -o json --show-secrets
secrets search -n refining --kind service --name gitea -o json
# Extract a single field value directly
secrets search -n refining --kind service --name gitea -f secret.token
# Extract a single metadata field directly
secrets search -n refining --kind service --name gitea -f metadata.url
# Pipe-friendly (non-TTY defaults to json-compact automatically)
secrets search -n refining --kind service | jq '.[].name'"
secrets search -n refining --kind service | jq '.[].name'
# Inject secrets into environment variables when you really need them
secrets inject -n refining --kind service --name gitea"
)]
struct Cli {
/// Database URL, overrides saved config (one-time override)
@@ -129,19 +132,19 @@ EXAMPLES:
# 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
# Extract a single metadata field value
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
-f metadata.url -f metadata.default_org
# Full JSON output with secrets revealed (ideal for AI parsing)
secrets search -n refining --kind service --name gitea -o json --show-secrets
# Export metadata as env vars (single record only)
secrets search -n refining --kind service --name gitea -o env
# Export as env vars (source-able; single record only)
secrets search -n refining --kind service --name gitea -o env --show-secrets
# Inject decrypted secrets only when needed
secrets inject -n refining --kind service --name gitea
secrets run -n refining --kind service --name gitea -- printenv
# Paginate large result sets
secrets search -n refining --summary --limit 10 --offset 0
@@ -151,8 +154,7 @@ EXAMPLES:
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'")]
secrets search -n refining --kind service | jq '.[].name'")]
Search {
/// Filter by namespace, e.g. refining, ricnsmart
#[arg(short, long)]
@@ -169,10 +171,10 @@ EXAMPLES:
/// Fuzzy keyword (matches name, namespace, kind, tags, metadata text)
#[arg(short, long)]
query: Option<String>,
/// Reveal encrypted secret values in output
/// Deprecated: search never reveals secrets; use inject/run instead
#[arg(long)]
show_secrets: bool,
/// Extract field value(s) directly: metadata.<key> or secret.<key> (repeatable)
/// Extract metadata field value(s) directly: metadata.<key> (repeatable)
#[arg(short = 'f', long = "field")]
fields: Vec<String>,
/// Return lightweight summary only (namespace, kind, name, tags, desc, updated_at)
@@ -501,9 +503,7 @@ async fn main() -> Result<()> {
sort,
output,
} => {
let master_key = crypto::load_master_key()?;
let _span = tracing::info_span!("cmd", command = "search").entered();
let show = show_secrets || fields.iter().any(|f| f.starts_with("secret"));
let out = resolve_output_mode(output.as_deref())?;
commands::search::run(
&pool,
@@ -513,7 +513,7 @@ async fn main() -> Result<()> {
name: name.as_deref(),
tags: &tag,
query: query.as_deref(),
show_secrets: show,
show_secrets,
fields: &fields,
summary,
limit,
@@ -521,7 +521,6 @@ async fn main() -> Result<()> {
sort: &sort,
output: out,
},
Some(&master_key),
)
.await?;
}

View File

@@ -1,3 +1,4 @@
use chrono::{DateTime, Local, Utc};
use std::io::IsTerminal;
use std::str::FromStr;
@@ -45,3 +46,10 @@ pub fn resolve_output_mode(explicit: Option<&str>) -> anyhow::Result<OutputMode>
Ok(OutputMode::JsonCompact)
}
}
/// Format a UTC timestamp for local human-readable output.
pub fn format_local_time(dt: DateTime<Utc>) -> String {
dt.with_timezone(&Local)
.format("%Y-%m-%d %H:%M:%S %:z")
.to_string()
}