bwx 0.2.0 → 0.2.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bwx",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "bitwarden",
@@ -1,47 +1,142 @@
1
1
  import type { Command } from "commander";
2
2
  import { getGlobalOpts } from "../program.ts";
3
3
  import { emitData } from "../io.ts";
4
- import { handleFatalError } from "../io.ts";
5
4
  import { CliError, ExitCode } from "../errors.ts";
6
5
  import { runBwOrThrow } from "../../bw/runner.ts";
7
6
  import { withSession } from "../../bw/session.ts";
7
+ import { BW_TYPE_LABELS, BwItemSchema } from "../../bw/types.ts";
8
+ import pc from "picocolors";
8
9
 
9
- const VALID_FIELDS = ["password", "username", "totp", "notes", "uri", "item"];
10
+ const BUILTIN_FIELDS = ["password", "username", "totp", "notes", "uri", "item"];
10
11
 
11
12
  export function registerGet(program: Command): void {
12
13
  program
13
14
  .command("get")
14
- .description("Get a field from a vault item")
15
- .argument("<field>", `Field: ${VALID_FIELDS.join(" | ")}`)
15
+ .description("Get a field from a vault item (built-in or custom)")
16
+ .argument("<field>", "Field: password | username | totp | notes | uri | item | <custom>")
16
17
  .argument("<item>", "Item name or ID")
17
18
  .action(async function (this: Command, field: string, item: string) {
18
19
  const opts = getGlobalOpts(this);
19
20
 
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
- );
21
+ if (BUILTIN_FIELDS.includes(field)) {
22
+ await getBuiltinField(field, item, opts);
23
+ } else {
24
+ await getCustomField(field, item, opts);
39
25
  }
26
+ });
27
+ }
40
28
 
29
+ async function getBuiltinField(
30
+ field: string,
31
+ item: string,
32
+ opts: ReturnType<typeof getGlobalOpts>,
33
+ ): Promise<void> {
34
+ try {
35
+ const result = await withSession(opts, async () => {
41
36
  if (field === "item") {
42
- emitData(JSON.parse(result), opts);
43
- } else {
44
- emitData(result, opts);
37
+ return runBwOrThrow(["get", "item", item]);
45
38
  }
39
+ return runBwOrThrow(["get", field, item]);
46
40
  });
41
+
42
+ if (!result) {
43
+ throw new CliError(
44
+ `No ${field} found for item: ${item}`,
45
+ ExitCode.NotFound,
46
+ );
47
+ }
48
+
49
+ if (field === "item") {
50
+ emitData(JSON.parse(result), opts);
51
+ } else {
52
+ emitData(result, opts);
53
+ }
54
+ } catch (err) {
55
+ if (
56
+ err instanceof CliError &&
57
+ /more than one result/i.test(err.message)
58
+ ) {
59
+ const matches = await resolveAmbiguous(item);
60
+ throw new CliError(
61
+ formatAmbiguousError(item, matches, opts),
62
+ ExitCode.BadArgs,
63
+ );
64
+ }
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ async function getCustomField(
70
+ fieldName: string,
71
+ item: string,
72
+ opts: ReturnType<typeof getGlobalOpts>,
73
+ ): Promise<void> {
74
+ const json = await withSession(opts, () =>
75
+ runBwOrThrow(["get", "item", item]),
76
+ );
77
+
78
+ const parsed = BwItemSchema.parse(JSON.parse(json));
79
+ const fields = parsed.fields ?? [];
80
+ const match = fields.find((f) => f.name === fieldName);
81
+
82
+ if (!match) {
83
+ const available = fields.map((f) => f.name).join(", ");
84
+ const msg = available
85
+ ? `Field "${fieldName}" not found. Available: ${available}`
86
+ : `Field "${fieldName}" not found (item has no custom fields)`;
87
+ throw new CliError(msg, ExitCode.NotFound);
88
+ }
89
+
90
+ emitData(match.value ?? "", opts);
91
+ }
92
+
93
+ interface MatchInfo {
94
+ id: string;
95
+ name: string;
96
+ type: string;
97
+ username?: string;
98
+ }
99
+
100
+ async function resolveAmbiguous(query: string): Promise<MatchInfo[]> {
101
+ try {
102
+ const json = await runBwOrThrow([
103
+ "list",
104
+ "items",
105
+ "--search",
106
+ query,
107
+ ]);
108
+ const items: Array<Record<string, unknown>> = JSON.parse(json);
109
+ return items.map((i) => ({
110
+ id: i.id as string,
111
+ name: i.name as string,
112
+ type: BW_TYPE_LABELS[i.type as number] ?? `type:${i.type}`,
113
+ username: (i.login as Record<string, unknown> | null)?.username as
114
+ | string
115
+ | undefined,
116
+ }));
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+
122
+ function formatAmbiguousError(
123
+ query: string,
124
+ matches: MatchInfo[],
125
+ opts: { json?: boolean },
126
+ ): string {
127
+ if (matches.length === 0) {
128
+ return `Multiple items match "${query}". Use a specific ID instead.`;
129
+ }
130
+
131
+ if (opts.json) {
132
+ return `Multiple items match "${query}": ${JSON.stringify(matches)}`;
133
+ }
134
+
135
+ const lines = [`Multiple items match "${query}":\n`];
136
+ for (const m of matches) {
137
+ const user = m.username ? ` (${m.username})` : "";
138
+ lines.push(` ${pc.dim(m.id)} ${pc.cyan(m.type)} ${m.name}${pc.dim(user)}`);
139
+ }
140
+ lines.push(`\nUse a specific ID: ${pc.dim("bwx get <field> <id>")}`);
141
+ return lines.join("\n");
47
142
  }
@@ -1,7 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import type { GlobalOptions } from "./types.ts";
3
3
  import { registerGet } from "./commands/get.ts";
4
- import { registerField } from "./commands/field.ts";
5
4
  import { registerSearch } from "./commands/search.ts";
6
5
  import { registerCreate } from "./commands/create.ts";
7
6
  import { registerEdit } from "./commands/edit.ts";
@@ -35,7 +34,6 @@ export function buildProgram(): Command {
35
34
  registerUnlock(program);
36
35
  registerLock(program);
37
36
  registerGet(program);
38
- registerField(program);
39
37
  registerSearch(program);
40
38
  registerSync(program);
41
39
  registerCreate(program);
@@ -1,36 +0,0 @@
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
- }