logicsrc/packages/cli/src/format.ts
Anthony Ettinger a0f8f2c125 vault + teams: filter secrets by category, export to CSV, simpler help
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>
2026-09-24 03:57:32 +00:00

37 lines
1.3 KiB
TypeScript

export type OutputFormat = "json" | "table" | "markdown";
export function print(data: unknown, format: OutputFormat) {
if (format === "json") {
console.log(JSON.stringify(data, null, 2));
return;
}
if (format === "markdown") {
if (Array.isArray(data)) {
for (const item of data) {
console.log(`- ${Object.entries(item as Record<string, unknown>).map(([key, value]) => `**${key}:** ${String(value)}`).join(", ")}`);
}
return;
}
console.log(Object.entries(data as Record<string, unknown>).map(([key, value]) => `**${key}:** ${String(value)}`).join("\n"));
return;
}
console.table(data);
}
/**
* A plain, aligned table: no index column, no quotes, no colour. Easier to read
* than console.table and pipes cleanly into grep, sort and awk.
*/
export function printColumns(rows: Array<Record<string, unknown>>): void {
if (rows.length === 0) return;
const headers = Object.keys(rows[0]!);
const cells = rows.map((row) => headers.map((h) => String(row[h] ?? "")));
const widths = headers.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i]!.length)));
const line = (values: string[]) =>
values.map((v, i) => (i === values.length - 1 ? v : v.padEnd(widths[i]!))).join(" ").trimEnd();
console.log(line(headers.map((h) => h.toUpperCase())));
for (const c of cells) console.log(line(c));
}