bwx 0.1.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/README.md +9 -2
- package/package.json +9 -1
- package/src/bw/types.ts +4 -0
- package/src/cli/commands/config.ts +1 -1
- package/src/cli/commands/create.ts +1 -1
- package/src/cli/commands/delete.ts +1 -1
- package/src/cli/commands/doctor.ts +157 -0
- package/src/cli/commands/get.ts +121 -26
- package/src/cli/commands/list.ts +81 -0
- package/src/cli/commands/search.ts +1 -1
- package/src/cli/program.ts +5 -3
- package/src/cli/commands/field.ts +0 -36
package/README.md
CHANGED
|
@@ -14,8 +14,11 @@ The native `bw` CLI requires interactive prompts and manual session management.
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
|
|
18
|
-
bun
|
|
17
|
+
# Global install
|
|
18
|
+
bun install -g bwx
|
|
19
|
+
|
|
20
|
+
# Or run without installing
|
|
21
|
+
bunx bwx doctor
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
Requires [Bitwarden CLI](https://bitwarden.com/help/cli/) (`bw`) installed and on PATH.
|
|
@@ -28,6 +31,9 @@ bwx config email your@email.com
|
|
|
28
31
|
|
|
29
32
|
# Store master password in macOS Keychain
|
|
30
33
|
bwx config master-password
|
|
34
|
+
|
|
35
|
+
# Verify everything is ready
|
|
36
|
+
bwx doctor
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
## Commands
|
|
@@ -40,6 +46,7 @@ bwx config master-password
|
|
|
40
46
|
| `bwx unlock` | Unlock vault & cache session |
|
|
41
47
|
| `bwx lock` | Lock vault & clear session |
|
|
42
48
|
| `bwx sync` | Sync vault from server |
|
|
49
|
+
| `bwx doctor` | Check setup (bw CLI, email, password, config) |
|
|
43
50
|
|
|
44
51
|
### Read
|
|
45
52
|
|
package/package.json
CHANGED
package/src/bw/types.ts
CHANGED
|
@@ -32,9 +32,13 @@ export const BW_TYPE_LABELS: Record<number, string> = {
|
|
|
32
32
|
|
|
33
33
|
export const BW_TYPE_FROM_NAME: Record<string, BwItemType> = {
|
|
34
34
|
login: BwItemType.Login,
|
|
35
|
+
logins: BwItemType.Login,
|
|
35
36
|
note: BwItemType.SecureNote,
|
|
37
|
+
notes: BwItemType.SecureNote,
|
|
36
38
|
card: BwItemType.Card,
|
|
39
|
+
cards: BwItemType.Card,
|
|
37
40
|
identity: BwItemType.Identity,
|
|
41
|
+
identities: BwItemType.Identity,
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
// --- Field types ---
|
|
@@ -6,7 +6,7 @@ import { loadConfig, saveConfig } from "../../config/store.ts";
|
|
|
6
6
|
|
|
7
7
|
export function registerConfig(program: Command): void {
|
|
8
8
|
const configCmd = program
|
|
9
|
-
.command("config")
|
|
9
|
+
.command("config").alias("cfg")
|
|
10
10
|
.description("Manage bwx configuration");
|
|
11
11
|
|
|
12
12
|
configCmd
|
|
@@ -13,7 +13,7 @@ function collect(val: string, prev: string[]): string[] {
|
|
|
13
13
|
|
|
14
14
|
export function registerCreate(program: Command): void {
|
|
15
15
|
program
|
|
16
|
-
.command("create")
|
|
16
|
+
.command("create").alias("add")
|
|
17
17
|
.description("Create a new vault item")
|
|
18
18
|
.option("--type <type>", "Item type: login|note", "note")
|
|
19
19
|
.option("--name <name>", "Item name")
|
|
@@ -8,7 +8,7 @@ import pc from "picocolors";
|
|
|
8
8
|
|
|
9
9
|
export function registerDelete(program: Command): void {
|
|
10
10
|
program
|
|
11
|
-
.command("delete")
|
|
11
|
+
.command("delete").alias("rm")
|
|
12
12
|
.description("Delete a vault item")
|
|
13
13
|
.argument("<item>", "Item name or ID")
|
|
14
14
|
.option("--permanent", "Permanently delete (no trash)")
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { getGlobalOpts } from "../program.ts";
|
|
3
|
+
import { emitData } from "../io.ts";
|
|
4
|
+
import { loadConfig } from "../../config/store.ts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
interface Check {
|
|
8
|
+
name: string;
|
|
9
|
+
ok: boolean;
|
|
10
|
+
detail?: string;
|
|
11
|
+
fix?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerDoctor(program: Command): void {
|
|
15
|
+
program
|
|
16
|
+
.command("doctor").alias("check")
|
|
17
|
+
.description("Check if bwx is ready to use")
|
|
18
|
+
.action(async function (this: Command) {
|
|
19
|
+
const opts = getGlobalOpts(this);
|
|
20
|
+
const checks: Check[] = [];
|
|
21
|
+
|
|
22
|
+
// 1. bw CLI installed
|
|
23
|
+
checks.push(await checkBwInstalled());
|
|
24
|
+
|
|
25
|
+
// 2. bw CLI version
|
|
26
|
+
if (checks[0]!.ok) {
|
|
27
|
+
checks.push(await checkBwVersion());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Email configured
|
|
31
|
+
checks.push(await checkEmail());
|
|
32
|
+
|
|
33
|
+
// 4. Master password in Keychain
|
|
34
|
+
checks.push(await checkMasterPassword());
|
|
35
|
+
|
|
36
|
+
// 5. Config dir writable
|
|
37
|
+
checks.push(await checkConfigDir());
|
|
38
|
+
|
|
39
|
+
if (opts.json || opts.plain) {
|
|
40
|
+
emitData(checks, opts);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const check of checks) {
|
|
45
|
+
const icon = check.ok ? pc.green("✓") : pc.red("✗");
|
|
46
|
+
const name = check.ok ? check.name : pc.red(check.name);
|
|
47
|
+
let line = `${icon} ${name}`;
|
|
48
|
+
if (check.detail) {
|
|
49
|
+
line += pc.dim(` (${check.detail})`);
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write(line + "\n");
|
|
52
|
+
if (!check.ok && check.fix) {
|
|
53
|
+
process.stderr.write(pc.dim(` → ${check.fix}`) + "\n");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
58
|
+
if (failed > 0) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
"\n" + pc.red(`${failed} issue${failed > 1 ? "s" : ""} found`) + "\n",
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
} else {
|
|
64
|
+
process.stderr.write("\n" + pc.green("All good") + "\n");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function checkBwInstalled(): Promise<Check> {
|
|
70
|
+
try {
|
|
71
|
+
const proc = Bun.spawn(["which", "bw"], {
|
|
72
|
+
stdout: "pipe",
|
|
73
|
+
stderr: "pipe",
|
|
74
|
+
});
|
|
75
|
+
const path = (await new Response(proc.stdout).text()).trim();
|
|
76
|
+
const code = await proc.exited;
|
|
77
|
+
if (code === 0 && path) {
|
|
78
|
+
return { name: "Bitwarden CLI", ok: true, detail: path };
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
return {
|
|
82
|
+
name: "Bitwarden CLI",
|
|
83
|
+
ok: false,
|
|
84
|
+
fix: "brew install bitwarden-cli",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function checkBwVersion(): Promise<Check> {
|
|
89
|
+
try {
|
|
90
|
+
const proc = Bun.spawn(["bw", "--version"], {
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
});
|
|
94
|
+
const version = (await new Response(proc.stdout).text()).trim();
|
|
95
|
+
const code = await proc.exited;
|
|
96
|
+
if (code === 0 && version) {
|
|
97
|
+
return { name: "bw version", ok: true, detail: version };
|
|
98
|
+
}
|
|
99
|
+
} catch {}
|
|
100
|
+
return { name: "bw version", ok: false, fix: "brew upgrade bitwarden-cli" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function checkEmail(): Promise<Check> {
|
|
104
|
+
const config = await loadConfig();
|
|
105
|
+
if (config.email) {
|
|
106
|
+
return { name: "Account email", ok: true, detail: config.email };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
name: "Account email",
|
|
110
|
+
ok: false,
|
|
111
|
+
fix: "bwx config email your@email.com",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function checkMasterPassword(): Promise<Check> {
|
|
116
|
+
try {
|
|
117
|
+
const proc = Bun.spawn(
|
|
118
|
+
[
|
|
119
|
+
"security",
|
|
120
|
+
"find-generic-password",
|
|
121
|
+
"-a",
|
|
122
|
+
"bitwarden",
|
|
123
|
+
"-s",
|
|
124
|
+
"bitwarden-master",
|
|
125
|
+
"-w",
|
|
126
|
+
],
|
|
127
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
128
|
+
);
|
|
129
|
+
const pw = (await new Response(proc.stdout).text()).trim();
|
|
130
|
+
const code = await proc.exited;
|
|
131
|
+
if (code === 0 && pw) {
|
|
132
|
+
return { name: "Master password", ok: true, detail: "in Keychain" };
|
|
133
|
+
}
|
|
134
|
+
} catch {}
|
|
135
|
+
return {
|
|
136
|
+
name: "Master password",
|
|
137
|
+
ok: false,
|
|
138
|
+
fix: "bwx config master-password",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function checkConfigDir(): Promise<Check> {
|
|
143
|
+
const configDir =
|
|
144
|
+
process.env.XDG_CONFIG_HOME ?? `${process.env.HOME}/.config`;
|
|
145
|
+
const dir = `${configDir}/bwx`;
|
|
146
|
+
try {
|
|
147
|
+
const { accessSync, constants } = await import("node:fs");
|
|
148
|
+
accessSync(dir, constants.W_OK);
|
|
149
|
+
return { name: "Config directory", ok: true, detail: dir };
|
|
150
|
+
} catch {}
|
|
151
|
+
return {
|
|
152
|
+
name: "Config directory",
|
|
153
|
+
ok: false,
|
|
154
|
+
detail: dir,
|
|
155
|
+
fix: `mkdir -p ${dir}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/cli/commands/get.ts
CHANGED
|
@@ -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
|
|
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>",
|
|
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 (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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 registerList(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command("list")
|
|
13
|
+
.alias("ls")
|
|
14
|
+
.description("List all vault items")
|
|
15
|
+
.argument("[type]", "Filter by type: logins|notes|cards|identities")
|
|
16
|
+
.option("--folder <id>", "Filter by folder ID")
|
|
17
|
+
.option("--limit <n>", "Limit results", parseInt)
|
|
18
|
+
.action(async function (
|
|
19
|
+
this: Command,
|
|
20
|
+
type: string | undefined,
|
|
21
|
+
localOpts: { folder?: string; limit?: number },
|
|
22
|
+
) {
|
|
23
|
+
const opts = getGlobalOpts(this);
|
|
24
|
+
|
|
25
|
+
const args = ["list", "items"];
|
|
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
|
+
if (type) {
|
|
34
|
+
const typeVal = BW_TYPE_FROM_NAME[type];
|
|
35
|
+
if (typeVal === undefined) {
|
|
36
|
+
throw new CliError(
|
|
37
|
+
`Invalid type "${type}". Valid: logins, notes, cards, identities`,
|
|
38
|
+
ExitCode.BadArgs,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
items = items.filter((i) => i.type === typeVal);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (localOpts.limit && localOpts.limit > 0) {
|
|
45
|
+
items = items.slice(0, localOpts.limit);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (items.length === 0) {
|
|
49
|
+
throw new CliError("No items found", ExitCode.NotFound);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
emitData(items, opts);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (opts.plain) {
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
const t =
|
|
60
|
+
BW_TYPE_LABELS[item.type as number] ?? `type:${item.type}`;
|
|
61
|
+
const login = item.login as Record<string, unknown> | null;
|
|
62
|
+
const username = login?.username ?? "-";
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
`${item.id}\t${t}\t${item.name}\t${username}\n`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const item of items) {
|
|
71
|
+
const t =
|
|
72
|
+
BW_TYPE_LABELS[item.type as number] ?? `type:${item.type}`;
|
|
73
|
+
const login = item.login as Record<string, unknown> | null;
|
|
74
|
+
const username = login?.username ?? "";
|
|
75
|
+
const userPart = username ? pc.dim(` (${username})`) : "";
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
`${pc.dim(item.id as string)} ${pc.cyan(t)} ${item.name}${userPart}\n`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -9,7 +9,7 @@ import pc from "picocolors";
|
|
|
9
9
|
|
|
10
10
|
export function registerSearch(program: Command): void {
|
|
11
11
|
program
|
|
12
|
-
.command("search")
|
|
12
|
+
.command("search").alias("find")
|
|
13
13
|
.description("Search vault items")
|
|
14
14
|
.argument("<query>", "Search query")
|
|
15
15
|
.option("--type <type>", "Filter by type: login|note|card|identity")
|
package/src/cli/program.ts
CHANGED
|
@@ -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";
|
|
@@ -11,6 +10,8 @@ import { registerSync } from "./commands/sync.ts";
|
|
|
11
10
|
import { registerUnlock } from "./commands/unlock.ts";
|
|
12
11
|
import { registerLock } from "./commands/lock.ts";
|
|
13
12
|
import { registerConfig } from "./commands/config.ts";
|
|
13
|
+
import { registerDoctor } from "./commands/doctor.ts";
|
|
14
|
+
import { registerList } from "./commands/list.ts";
|
|
14
15
|
|
|
15
16
|
export function buildProgram(): Command {
|
|
16
17
|
const program = new Command();
|
|
@@ -18,7 +19,7 @@ export function buildProgram(): Command {
|
|
|
18
19
|
program
|
|
19
20
|
.name("bwx")
|
|
20
21
|
.description("Bitwarden Extended CLI")
|
|
21
|
-
.version("0.
|
|
22
|
+
.version("0.2.0")
|
|
22
23
|
.option("--json", "JSON output")
|
|
23
24
|
.option("--plain", "Plain parseable output")
|
|
24
25
|
.option("-q, --quiet", "Suppress log output")
|
|
@@ -33,13 +34,14 @@ export function buildProgram(): Command {
|
|
|
33
34
|
registerUnlock(program);
|
|
34
35
|
registerLock(program);
|
|
35
36
|
registerGet(program);
|
|
36
|
-
registerField(program);
|
|
37
37
|
registerSearch(program);
|
|
38
38
|
registerSync(program);
|
|
39
39
|
registerCreate(program);
|
|
40
40
|
registerEdit(program);
|
|
41
41
|
registerDelete(program);
|
|
42
|
+
registerList(program);
|
|
42
43
|
registerConfig(program);
|
|
44
|
+
registerDoctor(program);
|
|
43
45
|
|
|
44
46
|
return program;
|
|
45
47
|
}
|
|
@@ -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
|
-
}
|