bwx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitSuccess } from "../io.ts";
4
+ import { CliError, ExitCode } from "../errors.ts";
5
+ import { loadConfig, saveConfig } from "../../config/store.ts";
6
+
7
+ export function registerConfig(program: Command): void {
8
+ const configCmd = program
9
+ .command("config")
10
+ .description("Manage bwx configuration");
11
+
12
+ configCmd
13
+ .command("email")
14
+ .description("Set Bitwarden account email")
15
+ .argument("[email]", "Account email address")
16
+ .action(async function (this: Command, email?: string) {
17
+ const opts = getGlobalOpts(this);
18
+
19
+ if (!email) {
20
+ if (!process.stdin.isTTY) {
21
+ throw new CliError(
22
+ "Pass email as argument when piping",
23
+ ExitCode.BadArgs,
24
+ );
25
+ }
26
+ process.stderr.write("Email: ");
27
+ email = await readLine();
28
+ }
29
+
30
+ if (!email) {
31
+ throw new CliError("Email is required", ExitCode.BadArgs);
32
+ }
33
+
34
+ const config = await loadConfig();
35
+ config.email = email;
36
+ await saveConfig(config);
37
+
38
+ emitSuccess("Email saved", opts);
39
+ });
40
+
41
+ configCmd
42
+ .command("master-password")
43
+ .description("Set master password in macOS Keychain")
44
+ .action(async function (this: Command) {
45
+ const opts = getGlobalOpts(this);
46
+
47
+ let password: string;
48
+
49
+ if (process.stdin.isTTY) {
50
+ // Interactive prompt
51
+ process.stderr.write("Master password: ");
52
+ password = await readLine();
53
+ } else {
54
+ // Piped input
55
+ const chunks: Uint8Array[] = [];
56
+ for await (const chunk of Bun.stdin.stream()) {
57
+ chunks.push(chunk);
58
+ }
59
+ password = Buffer.concat(chunks).toString().trim();
60
+ }
61
+
62
+ if (!password) {
63
+ throw new CliError("No password provided", ExitCode.BadArgs);
64
+ }
65
+
66
+ const proc = Bun.spawn(
67
+ [
68
+ "security",
69
+ "add-generic-password",
70
+ "-a",
71
+ "bitwarden",
72
+ "-s",
73
+ "bitwarden-master",
74
+ "-U",
75
+ "-w",
76
+ password,
77
+ ],
78
+ { stdout: "pipe", stderr: "pipe" },
79
+ );
80
+
81
+ const stderr = await new Response(proc.stderr).text();
82
+ const exitCode = await proc.exited;
83
+
84
+ if (exitCode !== 0) {
85
+ throw new CliError(
86
+ `Failed to save to Keychain: ${stderr.trim()}`,
87
+ ExitCode.Config,
88
+ );
89
+ }
90
+
91
+ emitSuccess("Master password saved to Keychain", opts);
92
+ });
93
+ }
94
+
95
+ async function readLine(): Promise<string> {
96
+ const chunks: Uint8Array[] = [];
97
+ for await (const chunk of Bun.stdin.stream()) {
98
+ chunks.push(chunk);
99
+ const text = Buffer.concat(chunks).toString();
100
+ if (text.includes("\n")) return text.split("\n")[0]!.trim();
101
+ }
102
+ return Buffer.concat(chunks).toString().trim();
103
+ }
@@ -0,0 +1,133 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData, emitSuccess } from "../io.ts";
4
+ import { CliError, ExitCode } from "../errors.ts";
5
+ import { runBwOrThrow } from "../../bw/runner.ts";
6
+ import { withSession } from "../../bw/session.ts";
7
+ import { buildNewItem, parseFieldFlag, bwEncode } from "../../bw/encoding.ts";
8
+
9
+ function collect(val: string, prev: string[]): string[] {
10
+ prev.push(val);
11
+ return prev;
12
+ }
13
+
14
+ export function registerCreate(program: Command): void {
15
+ program
16
+ .command("create")
17
+ .description("Create a new vault item")
18
+ .option("--type <type>", "Item type: login|note", "note")
19
+ .option("--name <name>", "Item name")
20
+ .option("--notes <text>", "Notes content")
21
+ .option("--username <user>", "Username (login type)")
22
+ .option("--password <pass>", "Password (login type)")
23
+ .option("--uri <url>", "URI (repeatable)", collect, [])
24
+ .option("--field <kv>", "Custom field k=v (repeatable)", collect, [])
25
+ .option("--folder <id>", "Folder ID")
26
+ .option("--favorite", "Mark as favorite")
27
+ .option("--from-json", "Read full item JSON from stdin")
28
+ .action(async function (
29
+ this: Command,
30
+ localOpts: {
31
+ type: string;
32
+ name?: string;
33
+ notes?: string;
34
+ username?: string;
35
+ password?: string;
36
+ uri: string[];
37
+ field: string[];
38
+ folder?: string;
39
+ favorite?: boolean;
40
+ fromJson?: boolean;
41
+ },
42
+ ) {
43
+ const opts = getGlobalOpts(this);
44
+
45
+ let encoded: string;
46
+
47
+ if (localOpts.fromJson) {
48
+ const stdin = await readStdin();
49
+ if (!stdin) {
50
+ throw new CliError("No JSON provided on stdin", ExitCode.BadArgs);
51
+ }
52
+ // Validate it's JSON
53
+ try {
54
+ JSON.parse(stdin);
55
+ } catch {
56
+ throw new CliError("Invalid JSON on stdin", ExitCode.BadArgs);
57
+ }
58
+ encoded = bwEncode(stdin);
59
+ } else {
60
+ if (!localOpts.name) {
61
+ throw new CliError("--name is required", ExitCode.BadArgs);
62
+ }
63
+
64
+ if (localOpts.type !== "login" && localOpts.type !== "note") {
65
+ throw new CliError(
66
+ `Invalid type "${localOpts.type}". Valid: login, note`,
67
+ ExitCode.BadArgs,
68
+ );
69
+ }
70
+
71
+ // Read notes from stdin if not provided and not a TTY
72
+ let notes = localOpts.notes ?? null;
73
+ if (notes === null && !process.stdin.isTTY) {
74
+ const stdinText = await readStdin();
75
+ if (stdinText) notes = stdinText;
76
+ }
77
+
78
+ // Read password from stdin if not provided, login type, and not a TTY
79
+ let password = localOpts.password ?? null;
80
+ if (
81
+ password === null &&
82
+ localOpts.type === "login" &&
83
+ !process.stdin.isTTY &&
84
+ notes === null
85
+ ) {
86
+ const stdinText = await readStdin();
87
+ if (stdinText) password = stdinText;
88
+ }
89
+
90
+ const fields = localOpts.field.map(parseFieldFlag);
91
+
92
+ const item = buildNewItem({
93
+ type: localOpts.type,
94
+ name: localOpts.name,
95
+ notes,
96
+ username: localOpts.username ?? null,
97
+ password,
98
+ uris: localOpts.uri,
99
+ fields,
100
+ folderId: localOpts.folder ?? null,
101
+ favorite: localOpts.favorite ?? false,
102
+ });
103
+
104
+ encoded = bwEncode(JSON.stringify(item));
105
+ }
106
+
107
+ const result = await withSession(opts, () =>
108
+ runBwOrThrow(["create", "item", encoded]),
109
+ );
110
+
111
+ const created = JSON.parse(result);
112
+ if (opts.json) {
113
+ emitData(created, opts);
114
+ } else if (opts.plain) {
115
+ emitData(created.id, opts);
116
+ } else {
117
+ emitSuccess(`Created "${created.name}" (${created.id})`, opts);
118
+ }
119
+ });
120
+ }
121
+
122
+ async function readStdin(): Promise<string | null> {
123
+ try {
124
+ const chunks: Uint8Array[] = [];
125
+ for await (const chunk of Bun.stdin.stream()) {
126
+ chunks.push(chunk);
127
+ }
128
+ const text = Buffer.concat(chunks).toString().trim();
129
+ return text || null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
@@ -0,0 +1,67 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData, emitSuccess } from "../io.ts";
4
+ import { CliError, ExitCode } from "../errors.ts";
5
+ import { runBwOrThrow } from "../../bw/runner.ts";
6
+ import { withSession } from "../../bw/session.ts";
7
+ import pc from "picocolors";
8
+
9
+ export function registerDelete(program: Command): void {
10
+ program
11
+ .command("delete")
12
+ .description("Delete a vault item")
13
+ .argument("<item>", "Item name or ID")
14
+ .option("--permanent", "Permanently delete (no trash)")
15
+ .option("--force", "Skip confirmation prompt")
16
+ .action(async function (
17
+ this: Command,
18
+ item: string,
19
+ localOpts: { permanent?: boolean; force?: boolean },
20
+ ) {
21
+ const opts = getGlobalOpts(this);
22
+
23
+ // Resolve item to get ID and name
24
+ const json = await withSession(opts, () =>
25
+ runBwOrThrow(["get", "item", item]),
26
+ );
27
+ const current = JSON.parse(json);
28
+ const itemId: string = current.id;
29
+ const itemName: string = current.name;
30
+
31
+ // Confirm unless --force, --json, or --plain
32
+ if (!localOpts.force && !opts.json && !opts.plain && process.stdin.isTTY) {
33
+ const label = localOpts.permanent ? "permanently delete" : "delete";
34
+ process.stderr.write(
35
+ `${pc.yellow("?")} ${label} "${itemName}" (${itemId})? [y/N] `,
36
+ );
37
+ const answer = await readLine();
38
+ if (answer.toLowerCase() !== "y") {
39
+ throw new CliError("Cancelled", ExitCode.UserCancelled);
40
+ }
41
+ }
42
+
43
+ const args = ["delete", "item", itemId];
44
+ if (localOpts.permanent) args.push("--permanent");
45
+
46
+ await withSession(opts, () => runBwOrThrow(args));
47
+
48
+ if (opts.json) {
49
+ emitData({ id: itemId, name: itemName, deleted: true }, opts);
50
+ } else if (opts.plain) {
51
+ emitData(itemId, opts);
52
+ } else {
53
+ const label = localOpts.permanent ? "Permanently deleted" : "Deleted";
54
+ emitSuccess(`${label} "${itemName}" (${itemId})`, opts);
55
+ }
56
+ });
57
+ }
58
+
59
+ async function readLine(): Promise<string> {
60
+ const chunks: Uint8Array[] = [];
61
+ for await (const chunk of Bun.stdin.stream()) {
62
+ chunks.push(chunk);
63
+ const text = Buffer.concat(chunks).toString();
64
+ if (text.includes("\n")) return text.split("\n")[0]!.trim();
65
+ }
66
+ return Buffer.concat(chunks).toString().trim();
67
+ }
@@ -0,0 +1,93 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData, emitSuccess } from "../io.ts";
4
+ import { CliError, ExitCode } from "../errors.ts";
5
+ import { runBwOrThrow } from "../../bw/runner.ts";
6
+ import { withSession } from "../../bw/session.ts";
7
+ import { patchItem, parseFieldFlag, bwEncode } from "../../bw/encoding.ts";
8
+
9
+ function collect(val: string, prev: string[]): string[] {
10
+ prev.push(val);
11
+ return prev;
12
+ }
13
+
14
+ export function registerEdit(program: Command): void {
15
+ program
16
+ .command("edit")
17
+ .description("Edit an existing vault item")
18
+ .argument("<item>", "Item name or ID")
19
+ .option("--name <name>", "New name")
20
+ .option("--notes <text>", "New notes")
21
+ .option("--username <user>", "New username")
22
+ .option("--password <pass>", "New password")
23
+ .option("--uri <url>", "Set URIs (repeatable, replaces all)", collect, [])
24
+ .option("--add-field <kv>", "Add/update field k=v", collect, [])
25
+ .option("--rm-field <name>", "Remove field by name", collect, [])
26
+ .option("--folder <id>", "Move to folder")
27
+ .option("--favorite", "Set favorite")
28
+ .option("--no-favorite", "Unset favorite")
29
+ .option("--from-json", "Read full item JSON from stdin (replaces item)")
30
+ .action(async function (
31
+ this: Command,
32
+ item: string,
33
+ localOpts: {
34
+ name?: string;
35
+ notes?: string;
36
+ username?: string;
37
+ password?: string;
38
+ uri: string[];
39
+ addField: string[];
40
+ rmField: string[];
41
+ folder?: string;
42
+ favorite?: boolean;
43
+ fromJson?: boolean;
44
+ },
45
+ ) {
46
+ const opts = getGlobalOpts(this);
47
+
48
+ const result = await withSession(opts, async () => {
49
+ // Fetch current item
50
+ const json = await runBwOrThrow(["get", "item", item]);
51
+ const current = JSON.parse(json);
52
+ const itemId = current.id;
53
+
54
+ let encoded: string;
55
+
56
+ if (localOpts.fromJson) {
57
+ const chunks: Uint8Array[] = [];
58
+ for await (const chunk of Bun.stdin.stream()) {
59
+ chunks.push(chunk);
60
+ }
61
+ const stdinText = Buffer.concat(chunks).toString().trim();
62
+ if (!stdinText) {
63
+ throw new CliError("No JSON provided on stdin", ExitCode.BadArgs);
64
+ }
65
+ encoded = bwEncode(stdinText);
66
+ } else {
67
+ const patched = patchItem(current, {
68
+ name: localOpts.name,
69
+ notes: localOpts.notes,
70
+ username: localOpts.username,
71
+ password: localOpts.password,
72
+ uris: localOpts.uri.length > 0 ? localOpts.uri : undefined,
73
+ addFields: localOpts.addField.map(parseFieldFlag),
74
+ rmFields: localOpts.rmField,
75
+ folderId: localOpts.folder,
76
+ favorite: localOpts.favorite,
77
+ });
78
+ encoded = bwEncode(JSON.stringify(patched));
79
+ }
80
+
81
+ return runBwOrThrow(["edit", "item", itemId, encoded]);
82
+ });
83
+
84
+ const updated = JSON.parse(result);
85
+ if (opts.json) {
86
+ emitData(updated, opts);
87
+ } else if (opts.plain) {
88
+ emitData(updated.id, opts);
89
+ } else {
90
+ emitSuccess(`Updated "${updated.name}" (${updated.id})`, opts);
91
+ }
92
+ });
93
+ }
@@ -0,0 +1,36 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData } from "../io.ts";
4
+ import { CliError, ExitCode } from "../errors.ts";
5
+ import { runBwOrThrow } from "../../bw/runner.ts";
6
+ import { withSession } from "../../bw/session.ts";
7
+ import { BwItemSchema } from "../../bw/types.ts";
8
+
9
+ export function registerField(program: Command): void {
10
+ program
11
+ .command("field")
12
+ .description("Get a custom field from a vault item")
13
+ .argument("<item>", "Item name or ID")
14
+ .argument("<field-name>", "Custom field name")
15
+ .action(async function (this: Command, item: string, fieldName: string) {
16
+ const opts = getGlobalOpts(this);
17
+
18
+ const json = await withSession(opts, () =>
19
+ runBwOrThrow(["get", "item", item]),
20
+ );
21
+
22
+ const parsed = BwItemSchema.parse(JSON.parse(json));
23
+ const fields = parsed.fields ?? [];
24
+ const match = fields.find((f) => f.name === fieldName);
25
+
26
+ if (!match) {
27
+ const available = fields.map((f) => f.name).join(", ");
28
+ const msg = available
29
+ ? `Field "${fieldName}" not found. Available: ${available}`
30
+ : `Field "${fieldName}" not found (item has no custom fields)`;
31
+ throw new CliError(msg, ExitCode.NotFound);
32
+ }
33
+
34
+ emitData(match.value ?? "", opts);
35
+ });
36
+ }
@@ -0,0 +1,47 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData } from "../io.ts";
4
+ import { handleFatalError } from "../io.ts";
5
+ import { CliError, ExitCode } from "../errors.ts";
6
+ import { runBwOrThrow } from "../../bw/runner.ts";
7
+ import { withSession } from "../../bw/session.ts";
8
+
9
+ const VALID_FIELDS = ["password", "username", "totp", "notes", "uri", "item"];
10
+
11
+ export function registerGet(program: Command): void {
12
+ program
13
+ .command("get")
14
+ .description("Get a field from a vault item")
15
+ .argument("<field>", `Field: ${VALID_FIELDS.join(" | ")}`)
16
+ .argument("<item>", "Item name or ID")
17
+ .action(async function (this: Command, field: string, item: string) {
18
+ const opts = getGlobalOpts(this);
19
+
20
+ if (!VALID_FIELDS.includes(field)) {
21
+ throw new CliError(
22
+ `Invalid field "${field}". Valid: ${VALID_FIELDS.join(", ")}`,
23
+ ExitCode.BadArgs,
24
+ );
25
+ }
26
+
27
+ const result = await withSession(opts, async () => {
28
+ if (field === "item") {
29
+ return runBwOrThrow(["get", "item", item]);
30
+ }
31
+ return runBwOrThrow(["get", field, item]);
32
+ });
33
+
34
+ if (!result) {
35
+ throw new CliError(
36
+ `No ${field} found for item: ${item}`,
37
+ ExitCode.NotFound,
38
+ );
39
+ }
40
+
41
+ if (field === "item") {
42
+ emitData(JSON.parse(result), opts);
43
+ } else {
44
+ emitData(result, opts);
45
+ }
46
+ });
47
+ }
@@ -0,0 +1,15 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitSuccess } from "../io.ts";
4
+ import { lockVault } from "../../bw/session.ts";
5
+
6
+ export function registerLock(program: Command): void {
7
+ program
8
+ .command("lock")
9
+ .description("Lock vault and clear cached session")
10
+ .action(async function (this: Command) {
11
+ const opts = getGlobalOpts(this);
12
+ await lockVault();
13
+ emitSuccess("Vault locked", opts);
14
+ });
15
+ }
@@ -0,0 +1,85 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData } from "../io.ts";
4
+ import { CliError, ExitCode } from "../errors.ts";
5
+ import { runBwOrThrow } from "../../bw/runner.ts";
6
+ import { withSession } from "../../bw/session.ts";
7
+ import { BW_TYPE_LABELS, BW_TYPE_FROM_NAME } from "../../bw/types.ts";
8
+ import pc from "picocolors";
9
+
10
+ export function registerSearch(program: Command): void {
11
+ program
12
+ .command("search")
13
+ .description("Search vault items")
14
+ .argument("<query>", "Search query")
15
+ .option("--type <type>", "Filter by type: login|note|card|identity")
16
+ .option("--folder <id>", "Filter by folder ID")
17
+ .option("--limit <n>", "Limit results", parseInt)
18
+ .action(async function (
19
+ this: Command,
20
+ query: string,
21
+ localOpts: { type?: string; folder?: string; limit?: number },
22
+ ) {
23
+ const opts = getGlobalOpts(this);
24
+
25
+ const args = ["list", "items", "--search", query];
26
+ if (localOpts.folder) {
27
+ args.push("--folderid", localOpts.folder);
28
+ }
29
+
30
+ const json = await withSession(opts, () => runBwOrThrow(args));
31
+ let items: Array<Record<string, unknown>> = JSON.parse(json);
32
+
33
+ // Filter by type
34
+ if (localOpts.type) {
35
+ const typeVal = BW_TYPE_FROM_NAME[localOpts.type];
36
+ if (typeVal === undefined) {
37
+ throw new CliError(
38
+ `Invalid type "${localOpts.type}". Valid: login, note, card, identity`,
39
+ ExitCode.BadArgs,
40
+ );
41
+ }
42
+ items = items.filter((i) => i.type === typeVal);
43
+ }
44
+
45
+ // Limit
46
+ if (localOpts.limit && localOpts.limit > 0) {
47
+ items = items.slice(0, localOpts.limit);
48
+ }
49
+
50
+ if (items.length === 0) {
51
+ throw new CliError(
52
+ `No items matching "${query}"`,
53
+ ExitCode.NotFound,
54
+ );
55
+ }
56
+
57
+ if (opts.json) {
58
+ emitData(items, opts);
59
+ return;
60
+ }
61
+
62
+ if (opts.plain) {
63
+ for (const item of items) {
64
+ const type = BW_TYPE_LABELS[item.type as number] ?? `type:${item.type}`;
65
+ const login = item.login as Record<string, unknown> | null;
66
+ const username = login?.username ?? "-";
67
+ process.stdout.write(
68
+ `${item.id}\t${type}\t${item.name}\t${username}\n`,
69
+ );
70
+ }
71
+ return;
72
+ }
73
+
74
+ // Human-friendly table
75
+ for (const item of items) {
76
+ const type = BW_TYPE_LABELS[item.type as number] ?? `type:${item.type}`;
77
+ const login = item.login as Record<string, unknown> | null;
78
+ const username = login?.username ?? "";
79
+ const userPart = username ? pc.dim(` (${username})`) : "";
80
+ process.stdout.write(
81
+ `${pc.dim(item.id as string)} ${pc.cyan(type)} ${item.name}${userPart}\n`,
82
+ );
83
+ }
84
+ });
85
+ }
@@ -0,0 +1,36 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitData } from "../io.ts";
4
+ import { getStatus } from "../../bw/session.ts";
5
+ import { BW_TYPE_LABELS } from "../../bw/types.ts";
6
+ import pc from "picocolors";
7
+
8
+ export function registerStatus(program: Command): void {
9
+ program
10
+ .command("status")
11
+ .description("Show vault status (no auto-unlock)")
12
+ .action(async function (this: Command) {
13
+ const opts = getGlobalOpts(this);
14
+ const status = await getStatus();
15
+
16
+ if (opts.json || opts.plain) {
17
+ emitData(status, opts);
18
+ return;
19
+ }
20
+
21
+ const statusColor =
22
+ status.status === "unlocked"
23
+ ? pc.green
24
+ : status.status === "locked"
25
+ ? pc.yellow
26
+ : pc.red;
27
+
28
+ const lines = [
29
+ `${pc.dim("Status:")} ${statusColor(status.status)}`,
30
+ `${pc.dim("Email:")} ${status.userEmail ?? "-"}`,
31
+ `${pc.dim("Server:")} ${status.serverUrl ?? "-"}`,
32
+ `${pc.dim("Synced:")} ${status.lastSync ?? "never"}`,
33
+ ];
34
+ process.stdout.write(lines.join("\n") + "\n");
35
+ });
36
+ }
@@ -0,0 +1,16 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitSuccess } from "../io.ts";
4
+ import { runBwOrThrow } from "../../bw/runner.ts";
5
+ import { withSession } from "../../bw/session.ts";
6
+
7
+ export function registerSync(program: Command): void {
8
+ program
9
+ .command("sync")
10
+ .description("Sync vault (auto-unlocks)")
11
+ .action(async function (this: Command) {
12
+ const opts = getGlobalOpts(this);
13
+ await withSession(opts, () => runBwOrThrow(["sync"]));
14
+ emitSuccess("Vault synced", opts);
15
+ });
16
+ }
@@ -0,0 +1,15 @@
1
+ import type { Command } from "commander";
2
+ import { getGlobalOpts } from "../program.ts";
3
+ import { emitSuccess } from "../io.ts";
4
+ import { ensureUnlocked } from "../../bw/session.ts";
5
+
6
+ export function registerUnlock(program: Command): void {
7
+ program
8
+ .command("unlock")
9
+ .description("Unlock vault (caches session)")
10
+ .action(async function (this: Command) {
11
+ const opts = getGlobalOpts(this);
12
+ await ensureUnlocked(opts);
13
+ emitSuccess("Vault unlocked", opts);
14
+ });
15
+ }
@@ -0,0 +1,40 @@
1
+ export const ExitCode = {
2
+ OK: 0,
3
+ Unknown: 1,
4
+ BadArgs: 2,
5
+ AuthFailed: 3,
6
+ NotFound: 4,
7
+ BwError: 5,
8
+ Network: 6,
9
+ Config: 7,
10
+ Timeout: 8,
11
+ UserCancelled: 9,
12
+ } as const;
13
+
14
+ export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
15
+
16
+ export class CliError extends Error {
17
+ constructor(
18
+ message: string,
19
+ public readonly exitCode: ExitCode = ExitCode.Unknown,
20
+ ) {
21
+ super(message);
22
+ this.name = "CliError";
23
+ }
24
+ }
25
+
26
+ const BW_STDERR_PATTERNS: Array<[RegExp, ExitCode]> = [
27
+ [/not logged in|unauthenticated/i, ExitCode.AuthFailed],
28
+ [/vault is locked/i, ExitCode.AuthFailed],
29
+ [/not found/i, ExitCode.NotFound],
30
+ [/more than one result/i, ExitCode.BadArgs],
31
+ [/timed? ?out/i, ExitCode.Timeout],
32
+ [/network|ECONNREFUSED|ENOTFOUND/i, ExitCode.Network],
33
+ ];
34
+
35
+ export function classifyBwError(stderr: string): ExitCode {
36
+ for (const [pattern, code] of BW_STDERR_PATTERNS) {
37
+ if (pattern.test(stderr)) return code;
38
+ }
39
+ return ExitCode.BwError;
40
+ }