feat: secrets list --json machine-readable output (EGB-699)

Add a --json flag to `secrets list` that emits a structured object for
tooling/CI instead of the human table — feeds the EGB-671 install scripts,
which need to enumerate a cloned store programmatically.

Contract: {"store", "projects":[{"name","entries":[...]}]}, each entry
self-describing via a type discriminator — {type:dotenv,path} or
{type:external,subtype:properties|file,path}. cmd_list_json mirrors the same
recursive store walk as the human list (nested <project>/<relpath>.age +
external/<slug>.age); jq assembles the JSON so paths escape correctly and
stdout stays pure JSON (the non-default-store hint is suppressed; jq is a
hard dep only in --json mode).

Tests: 7 new bats cases (dotenv, nested relpath, external properties + file
subtypes, empty store, pure-stdout-under-notice, store path). Full suite
261 pass / 0 fail.

VERSION 0.7.1.0 -> 0.7.2.0; CHANGELOG/README/CLAUDE.md updated.
This commit is contained in:
Brian Majewski 2026-06-08 13:56:45 -07:00
parent b8fe20f9bf
commit 446256caf1
6 changed files with 172 additions and 4 deletions

73
secrets
View file

@ -1744,9 +1744,21 @@ cmd_pull_workspaces() {
}
cmd_list() {
local json=0
case "${1:-}" in
--json) json=1 ;;
"") ;;
*) die "Usage: secrets list [--json]" ;;
esac
resolve_store
check_initialized
if [ "$json" -eq 1 ]; then
cmd_list_json
return
fi
local found=0
for dir in "$SECRETS_DIR"/*/; do
[ -d "$dir" ] || continue
@ -1782,6 +1794,64 @@ cmd_list() {
fi
}
# EGB-699: machine-readable listing for tooling/CI (feeds EGB-671 install
# scripts). Contract: a single JSON object on stdout —
# {"store": "<dir>", "projects": [{"name", "entries": [...]}]}
# where each entry is {"type":"dotenv","path":<relpath>} or
# {"type":"external","subtype":"properties"|"file","path":<slug>}. Mirrors the
# recursive store walk the human `list` uses (nested <project>/<relpath>.age +
# external/<slug>.age). jq does the assembly so paths are escaped correctly;
# stdout stays pure JSON (the human store hint is suppressed in this mode).
cmd_list_json() {
check_cmd jq
{
for dir in "$SECRETS_DIR"/*/; do
[ -d "$dir" ] || continue
local project
project=$(basename "$dir")
[[ "$project" == .* ]] && continue
# Marker line so a project with zero blobs still appears (mirrors the
# human header), grouped via jq below.
printf 'project\t%s\n' "$project"
local f rel name
while IFS= read -r f; do
[ -f "$f" ] || continue
rel=${f#"$dir"}
rel=${rel%.age}
case "$rel" in
external/*)
name=${rel#external/}
case "$name" in
*.file) printf 'entry\t%s\texternal\tfile\t%s\n' "$project" "${name%.file}" ;;
*.properties) printf 'entry\t%s\texternal\tproperties\t%s\n' "$project" "${name%.properties}" ;;
*.gradle-properties) printf 'entry\t%s\texternal\tproperties\t%s\n' "$project" "${name%.gradle-properties}" ;;
*) printf 'entry\t%s\texternal\tunknown\t%s\n' "$project" "$name" ;;
esac
;;
*)
printf 'entry\t%s\tdotenv\t\t%s\n' "$project" "$rel"
;;
esac
done < <(find "$dir" -type f -name '*.age' | sort)
done
} | jq -R -n --arg store "$SECRETS_DIR" '
[inputs | split("\t")] as $lines
| ($lines | map(select(.[0] == "project") | .[1]) | unique) as $names
| {
store: $store,
projects: ($names | map(. as $p | {
name: $p,
entries: [ $lines[]
| select(.[0] == "entry" and .[1] == $p)
| if .[2] == "external"
then { type: "external", subtype: .[3], path: .[4] }
else { type: "dotenv", path: .[4] }
end ]
}))
}'
}
cmd_rm() {
check_cmd git
resolve_store
@ -2464,6 +2534,7 @@ Usage:
secrets clear -w|--workspaces Clear secrets from all workspaces in package.json
secrets run [-w] <command> Pull secrets, run command, clear secrets on exit
secrets list List all projects and their secret files
secrets list --json Same listing as machine-readable JSON (for tooling/CI)
secrets rm <project> Remove a project's secrets from the repo
secrets rekey Re-encrypt all secrets with a new key
secrets verify [project] Check the manifest against the store + decrypt every blob
@ -2636,7 +2707,7 @@ case "${1:-help}" in
cmd_run "$@"
;;
add) cmd_add "${2:-}" ;;
list) cmd_list ;;
list) shift; cmd_list "$@" ;;
rm) cmd_rm "${2:-}" ;;
rekey) cmd_rekey ;;
verify) shift; cmd_verify "$@" ;;