vault + teams: filter secrets by category, export to CSV, simpler help (#195)

Every secret now has a category derived from its name (db, social, server,
api, cloud, finance, crypto, ai, email, messaging, storage, dns, analytics,
devtools, auth, config, other). One rule table in @logicsrc/opencreds serves
both vaults; services win over generic words, so STRIPE_WEBHOOK_SECRET is
finance, not auth. Checked against the 1,208 distinct key names in the
profullstack team: 74 fall to "other".

Team vaults (where the shared .env secrets live):
- teams categories [team]    the filter words, with per-category counts
- teams secrets <team> [project] [env] --category/-c --search/-s
                             names + categories, never decrypts; --format csv
- teams export  <team> [project] [env] --category -o file.csv [--yes]
                             decrypts into team,project,env,category,key,
                             value,updated_at (0600); skips vaults without a
                             grant and names them

Personal vault (OpenCreds):
- vault list --category, and the category column in list output
- vault export --format csv: one flat row per item, keeps key/account
  secrets that a Bitwarden CSV drops; --category on every export format

DX:
- examples in `logicsrc vault help` / -h / --help that start by saying which
  of the two vaults you want, plus examples on teams and each subcommand
- password prompts go to stderr, so eval "$(logicsrc vault unlock)" works
- hints name the command you actually ran (logicsrc vault init, not
  opencreds init)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-09-23 21:08:08 -07:00 • committed by GitHub
parent 156c9164a9
commit 3d155af970
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 846 additions and 19 deletions

View file

@ -8,7 +8,9 @@
*/
import { createInterface } from "node:readline";
import { stdin, stdout } from "node:process";
// Prompts go to stderr so stdout carries only the answer: `eval "$(opencreds
// unlock)"` then captures the export line and nothing else.
import { stdin, stderr } from "node:process";
/** Read a line with the terminal's echo turned off. */
export async function promptSecret(label: string): Promise<string> {
@ -18,7 +20,7 @@ export async function promptSecret(label: string): Promise<string> {
return readLineFromStdin();
}
const rl = createInterface({ input: stdin, output: stdout, terminal: true });
const rl = createInterface({ input: stdin, output: stderr, terminal: true });
const asMutable = rl as unknown as { output: { write: (chunk: string) => void }; _writeToOutput?: (s: string) => void };
let muted = false;
@ -51,7 +53,7 @@ export async function promptNewSecret(label: string, confirmLabel = "Repeat: "):
}
export async function promptLine(label: string): Promise<string> {
const rl = createInterface({ input: stdin, output: stdout });
const rl = createInterface({ input: stdin, output: stderr });
const answer = await new Promise<string>((resolve) => rl.question(label, resolve));
rl.close();
return answer;