bwx 0.8.0 → 0.10.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.
- package/LICENSE +21 -0
- package/README.md +100 -7
- package/package.json +23 -6
- package/skill/bwx/SKILL.md +75 -7
- package/src/bw/card-input.ts +180 -0
- package/src/bw/discovery.ts +59 -0
- package/src/bw/encoding.ts +32 -5
- package/src/bw/fields.ts +83 -11
- package/src/bw/items.ts +1 -2
- package/src/bw/references.ts +58 -0
- package/src/bw/safe-errors.ts +24 -0
- package/src/bw/types.ts +16 -0
- package/src/cli/card-options.ts +169 -0
- package/src/cli/commands/create.ts +40 -18
- package/src/cli/commands/edit.ts +40 -17
- package/src/cli/commands/get.ts +32 -11
- package/src/cli/commands/run.ts +15 -14
- package/src/cli/commands/search.ts +30 -13
- package/src/cli/input.ts +159 -54
- package/src/cli/items.ts +26 -1
|
@@ -19,9 +19,16 @@ import {
|
|
|
19
19
|
StdinReader,
|
|
20
20
|
type ItemTextInputs,
|
|
21
21
|
} from "../input.ts";
|
|
22
|
+
import {
|
|
23
|
+
assertCardIntent,
|
|
24
|
+
registerCardOptions,
|
|
25
|
+
resolveCardPatch,
|
|
26
|
+
usedCardOnlyFlags,
|
|
27
|
+
type CardOptionInputs,
|
|
28
|
+
} from "../card-options.ts";
|
|
22
29
|
import { redactGeneratedPassword } from "./generate.ts";
|
|
23
30
|
|
|
24
|
-
interface CreateOptions extends ItemTextInputs {
|
|
31
|
+
interface CreateOptions extends ItemTextInputs, CardOptionInputs {
|
|
25
32
|
type: string;
|
|
26
33
|
name?: string;
|
|
27
34
|
username?: string;
|
|
@@ -35,7 +42,10 @@ interface CreateOptions extends ItemTextInputs {
|
|
|
35
42
|
generateLength?: number;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
const ITEM_TYPES = ["login", "note", "card"] as const;
|
|
46
|
+
type CreateItemType = (typeof ITEM_TYPES)[number];
|
|
47
|
+
|
|
48
|
+
/** Flags only a login has anywhere to store — see {@link assertTypeFlags}. */
|
|
39
49
|
const LOGIN_ONLY_FLAGS: Array<[keyof CreateOptions, string]> = [
|
|
40
50
|
["username", "--username"],
|
|
41
51
|
...loginOnlyTextInputFlags(),
|
|
@@ -45,11 +55,12 @@ export function registerCreate(program: Command): void {
|
|
|
45
55
|
const command = program
|
|
46
56
|
.command("create").alias("add")
|
|
47
57
|
.description("Create a new vault item")
|
|
48
|
-
.option("--type <type>", "Item type: login|note", "note")
|
|
58
|
+
.option("--type <type>", "Item type: login|note|card", "note")
|
|
49
59
|
.option("--name <name>", "Item name")
|
|
50
60
|
.option("--username <user>", "Username (login items only)");
|
|
51
61
|
|
|
52
|
-
registerItemTextOptions(command)
|
|
62
|
+
registerItemTextOptions(command);
|
|
63
|
+
registerCardOptions(command)
|
|
53
64
|
.option("--uri <url>", "URI (repeatable)", collect, [])
|
|
54
65
|
.option("--field <kv>", "Custom field k=v (repeatable)", collect, [])
|
|
55
66
|
.option("--field-file <kv>", "Custom field value from file k=path (repeatable)", collect, [])
|
|
@@ -84,14 +95,16 @@ export function registerCreate(program: Command): void {
|
|
|
84
95
|
throw new CliError("--name is required", ExitCode.BadArgs);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
if (
|
|
98
|
+
if (!ITEM_TYPES.includes(localOpts.type as CreateItemType)) {
|
|
88
99
|
throw new CliError(
|
|
89
|
-
`Invalid type "${localOpts.type}". Valid:
|
|
100
|
+
`Invalid type "${localOpts.type}". Valid: ${ITEM_TYPES.join(", ")}`,
|
|
90
101
|
ExitCode.BadArgs,
|
|
91
102
|
);
|
|
92
103
|
}
|
|
104
|
+
const type = localOpts.type as CreateItemType;
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
assertTypeFlags(localOpts, type);
|
|
107
|
+
assertCardIntent(localOpts);
|
|
95
108
|
|
|
96
109
|
const explicitStdinSources = listExplicitStdinInputs({
|
|
97
110
|
...localOpts,
|
|
@@ -126,13 +139,17 @@ export function registerCreate(program: Command): void {
|
|
|
126
139
|
});
|
|
127
140
|
|
|
128
141
|
const item = buildNewItem({
|
|
129
|
-
type
|
|
142
|
+
type,
|
|
130
143
|
name: localOpts.name,
|
|
131
144
|
notes: notes ?? null,
|
|
132
145
|
username: localOpts.username ?? null,
|
|
133
146
|
password: textInputs.password ?? null,
|
|
134
147
|
totp: textInputs.totp ?? null,
|
|
135
148
|
uris: localOpts.uri,
|
|
149
|
+
card:
|
|
150
|
+
type === "card"
|
|
151
|
+
? resolveCardPatch(localOpts, textInputs)
|
|
152
|
+
: undefined,
|
|
136
153
|
fields,
|
|
137
154
|
folderId: localOpts.folder
|
|
138
155
|
? await resolveFolderRef(localOpts.folder, opts)
|
|
@@ -160,21 +177,26 @@ export function registerCreate(program: Command): void {
|
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* "store" a credential, get exit 0,
|
|
180
|
+
* `buildNewItem` writes only the object the item type has — `login` for a login,
|
|
181
|
+
* `card` for a card — so any other type's values would be dropped on the floor.
|
|
182
|
+
* Accepting them silently means a caller can "store" a credential, get exit 0,
|
|
183
|
+
* and end up with an item that never held it.
|
|
166
184
|
*/
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const used = LOGIN_ONLY_FLAGS.filter(([key]) => Boolean(localOpts[key])).map(
|
|
185
|
+
function assertTypeFlags(localOpts: CreateOptions, type: CreateItemType): void {
|
|
186
|
+
const login = LOGIN_ONLY_FLAGS.filter(([key]) => Boolean(localOpts[key])).map(
|
|
171
187
|
([, flag]) => flag,
|
|
172
188
|
);
|
|
173
|
-
if (localOpts.uri.length > 0)
|
|
189
|
+
if (localOpts.uri.length > 0) login.push("--uri");
|
|
190
|
+
|
|
191
|
+
const required: Array<[CreateItemType, string[]]> = [
|
|
192
|
+
["login", login],
|
|
193
|
+
["card", usedCardOnlyFlags(localOpts)],
|
|
194
|
+
];
|
|
174
195
|
|
|
175
|
-
|
|
196
|
+
for (const [needed, used] of required) {
|
|
197
|
+
if (needed === type || used.length === 0) continue;
|
|
176
198
|
throw new CliError(
|
|
177
|
-
`${used.join(", ")} ${used.length > 1 ? "need" : "needs"} --type
|
|
199
|
+
`${used.join(", ")} ${used.length > 1 ? "need" : "needs"} --type ${needed} (a ${type} cannot store them).`,
|
|
178
200
|
ExitCode.BadArgs,
|
|
179
201
|
);
|
|
180
202
|
}
|
package/src/cli/commands/edit.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { withSession } from "../../bw/session.ts";
|
|
|
7
7
|
import { patchItem, bwEncode } from "../../bw/encoding.ts";
|
|
8
8
|
import { generateSecret } from "../../bw/generate.ts";
|
|
9
9
|
import { resolveFolderRef } from "../../bw/folders.ts";
|
|
10
|
-
import { BwItemType } from "../../bw/types.ts";
|
|
10
|
+
import { BwItemType, itemTypeLabel, type BwCard } from "../../bw/types.ts";
|
|
11
11
|
import {
|
|
12
12
|
assertSingleStdinSource,
|
|
13
13
|
collect,
|
|
@@ -21,9 +21,18 @@ import {
|
|
|
21
21
|
TOTP_FAMILY,
|
|
22
22
|
type ItemTextInputs,
|
|
23
23
|
} from "../input.ts";
|
|
24
|
+
import {
|
|
25
|
+
assertCardIntent,
|
|
26
|
+
registerCardOptions,
|
|
27
|
+
registerCardRemovalOptions,
|
|
28
|
+
resolveCardPatch,
|
|
29
|
+
usedCardOnlyFlags,
|
|
30
|
+
type CardOptionInputs,
|
|
31
|
+
type CardRemovalInputs,
|
|
32
|
+
} from "../card-options.ts";
|
|
24
33
|
import { redactGeneratedPassword } from "./generate.ts";
|
|
25
34
|
|
|
26
|
-
interface EditOptions extends ItemTextInputs {
|
|
35
|
+
interface EditOptions extends ItemTextInputs, CardOptionInputs, CardRemovalInputs {
|
|
27
36
|
name?: string;
|
|
28
37
|
username?: string;
|
|
29
38
|
uri: string[];
|
|
@@ -52,8 +61,12 @@ export function registerEdit(program: Command): void {
|
|
|
52
61
|
.option("--name <name>", "New name")
|
|
53
62
|
.option("--username <user>", "New username");
|
|
54
63
|
|
|
55
|
-
registerItemTextOptions(command)
|
|
56
|
-
|
|
64
|
+
registerItemTextOptions(command).option(
|
|
65
|
+
"--rm-totp",
|
|
66
|
+
"Remove the stored TOTP secret",
|
|
67
|
+
);
|
|
68
|
+
registerCardOptions(command);
|
|
69
|
+
registerCardRemovalOptions(command)
|
|
57
70
|
.option("--uri <url>", "Set URIs (repeatable, replaces all)", collect, [])
|
|
58
71
|
.option("--add-field <kv>", "Add/update field k=v", collect, [])
|
|
59
72
|
.option("--add-field-file <kv>", "Add/update field value from file k=path", collect, [])
|
|
@@ -87,8 +100,9 @@ export function registerEdit(program: Command): void {
|
|
|
87
100
|
}
|
|
88
101
|
encoded = bwEncode(stdinText);
|
|
89
102
|
} else {
|
|
90
|
-
|
|
103
|
+
assertTypeFlags(localOpts, current);
|
|
91
104
|
assertTotpIntent(localOpts);
|
|
105
|
+
assertCardIntent(localOpts);
|
|
92
106
|
|
|
93
107
|
assertSingleStdinSource(
|
|
94
108
|
listExplicitStdinInputs({
|
|
@@ -119,6 +133,10 @@ export function registerEdit(program: Command): void {
|
|
|
119
133
|
password: textInputs.password,
|
|
120
134
|
totp: localOpts.rmTotp ? null : textInputs.totp,
|
|
121
135
|
uris: localOpts.uri.length > 0 ? localOpts.uri : undefined,
|
|
136
|
+
card:
|
|
137
|
+
current.type === BwItemType.Card
|
|
138
|
+
? resolveCardPatch(localOpts, textInputs, current.card as BwCard | null)
|
|
139
|
+
: undefined,
|
|
122
140
|
addFields,
|
|
123
141
|
rmFields: localOpts.rmField,
|
|
124
142
|
folderId: localOpts.folder
|
|
@@ -162,24 +180,29 @@ function assertTotpIntent(localOpts: EditOptions): void {
|
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
/**
|
|
165
|
-
* Same trap as `create`: only a login
|
|
166
|
-
*
|
|
183
|
+
* Same trap as `create`: only a login has somewhere to put a password and only a
|
|
184
|
+
* card somewhere to put a card number, so setting either on another type would
|
|
185
|
+
* be accepted and then quietly lost.
|
|
167
186
|
*/
|
|
168
|
-
function
|
|
187
|
+
function assertTypeFlags(
|
|
169
188
|
localOpts: EditOptions,
|
|
170
189
|
current: Record<string, unknown>,
|
|
171
190
|
): void {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const used = LOGIN_ONLY_FLAGS.filter(([key]) => Boolean(localOpts[key])).map(
|
|
191
|
+
const login = LOGIN_ONLY_FLAGS.filter(([key]) => Boolean(localOpts[key])).map(
|
|
175
192
|
([, flag]) => flag,
|
|
176
193
|
);
|
|
177
|
-
if (localOpts.uri.length > 0)
|
|
194
|
+
if (localOpts.uri.length > 0) login.push("--uri");
|
|
178
195
|
|
|
179
|
-
|
|
196
|
+
const required: Array<[number, string[]]> = [
|
|
197
|
+
[BwItemType.Login, login],
|
|
198
|
+
[BwItemType.Card, usedCardOnlyFlags(localOpts)],
|
|
199
|
+
];
|
|
180
200
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
for (const [needed, used] of required) {
|
|
202
|
+
if (needed === current.type || used.length === 0) continue;
|
|
203
|
+
throw new CliError(
|
|
204
|
+
`${used.join(", ")} ${used.length > 1 ? "need" : "needs"} a ${itemTypeLabel(needed)} item, but "${String(current.name)}" is a ${itemTypeLabel(Number(current.type))}.`,
|
|
205
|
+
ExitCode.BadArgs,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
185
208
|
}
|
package/src/cli/commands/get.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { getGlobalOpts } from "../program.ts";
|
|
3
3
|
import { emitData, emitLog, emitRaw } from "../io.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BUILTIN_FIELDS, readItemField, readTotp, readTotpSecret, type FieldRequest,
|
|
6
|
+
} from "../../bw/fields.ts";
|
|
5
7
|
import { CARD_FIELDS } from "../../bw/cards.ts";
|
|
6
8
|
import { CliError, ExitCode } from "../errors.ts";
|
|
9
|
+
import { parseFieldReference, REFERENCE_PREFIX } from "../../bw/references.ts";
|
|
7
10
|
import type { GlobalOptions } from "../types.ts";
|
|
8
11
|
|
|
9
12
|
/** Seconds left below which `--fresh` waits, when the flag is given bare. */
|
|
@@ -22,12 +25,12 @@ interface GetOptions {
|
|
|
22
25
|
export function registerGet(program: Command): void {
|
|
23
26
|
program
|
|
24
27
|
.command("get")
|
|
25
|
-
.description("Get a field from a vault item
|
|
28
|
+
.description("Get a field from a vault item or a discovered bwx:// reference")
|
|
26
29
|
.argument(
|
|
27
30
|
"<field>",
|
|
28
|
-
`Field: ${BUILTIN_FIELDS.join(" | ")} | <custom>. Card items also take: ${CARD_FIELDS.join(" | ")}`,
|
|
31
|
+
`Field or bwx:// ref: ${BUILTIN_FIELDS.join(" | ")} | <custom>. Card items also take: ${CARD_FIELDS.join(" | ")}`,
|
|
29
32
|
)
|
|
30
|
-
.argument("
|
|
33
|
+
.argument("[item]", "Item name or ID (omit when the field is a bwx:// reference)")
|
|
31
34
|
.option(
|
|
32
35
|
"--fresh [seconds]",
|
|
33
36
|
`Wait for the next TOTP window if fewer than N seconds remain (default ${DEFAULT_FRESH_SECONDS})`,
|
|
@@ -37,26 +40,28 @@ export function registerGet(program: Command): void {
|
|
|
37
40
|
.action(async function (
|
|
38
41
|
this: Command,
|
|
39
42
|
field: string,
|
|
40
|
-
item: string,
|
|
43
|
+
item: string | undefined,
|
|
41
44
|
localOpts: GetOptions,
|
|
42
45
|
) {
|
|
43
46
|
const opts = getGlobalOpts(this);
|
|
44
47
|
assertRawUsable(localOpts, opts);
|
|
48
|
+
const request = parseGetRequest(field, item);
|
|
45
49
|
|
|
46
|
-
if (field === "totp") {
|
|
47
|
-
await emitTotp(item, localOpts, opts);
|
|
50
|
+
if (request.field === "totp" && !request.options?.customOnly) {
|
|
51
|
+
await emitTotp(request.item, localOpts, opts, request.options?.exactItemId);
|
|
48
52
|
return;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
assertTotpOnlyFlags(field, localOpts);
|
|
55
|
+
assertTotpOnlyFlags(request.field, localOpts);
|
|
52
56
|
|
|
53
|
-
const value = await readItemField(field, item, opts, {
|
|
57
|
+
const value = await readItemField(request.field, request.item, opts, {
|
|
58
|
+
...request.options,
|
|
54
59
|
verbatim: localOpts.raw,
|
|
55
60
|
});
|
|
56
61
|
|
|
57
62
|
// `item` is the one field that is itself JSON, so it is emitted as
|
|
58
63
|
// structure — except under `--raw`, which promised the bytes `bw` gave us.
|
|
59
|
-
if (field === "item" && !localOpts.raw) {
|
|
64
|
+
if (request.field === "item" && !request.options?.customOnly && !localOpts.raw) {
|
|
60
65
|
emitData(JSON.parse(value), opts);
|
|
61
66
|
return;
|
|
62
67
|
}
|
|
@@ -90,6 +95,20 @@ export function registerGet(program: Command): void {
|
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
function parseGetRequest(field: string, item: string | undefined): FieldRequest {
|
|
99
|
+
if (field.startsWith(REFERENCE_PREFIX)) {
|
|
100
|
+
const reference = parseFieldReference(field);
|
|
101
|
+
if (item !== undefined) {
|
|
102
|
+
throw new CliError("A field reference already contains the item ID; omit <item>.", ExitCode.BadArgs);
|
|
103
|
+
}
|
|
104
|
+
return reference;
|
|
105
|
+
}
|
|
106
|
+
if (item === undefined) {
|
|
107
|
+
throw new CliError("Expected get <field> <item> or get <ref>.", ExitCode.BadArgs);
|
|
108
|
+
}
|
|
109
|
+
return { field, item };
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
/**
|
|
94
113
|
* Emits one field value. `--raw` writes it alone, so piping a secret into a
|
|
95
114
|
* consumer that reads stdin literally does not append bwx's newline to it;
|
|
@@ -131,6 +150,7 @@ async function emitTotp(
|
|
|
131
150
|
item: string,
|
|
132
151
|
localOpts: GetOptions,
|
|
133
152
|
opts: GlobalOptions,
|
|
153
|
+
exactItemId = false,
|
|
134
154
|
): Promise<void> {
|
|
135
155
|
if (localOpts.seed) {
|
|
136
156
|
if (localOpts.fresh !== undefined) {
|
|
@@ -139,11 +159,12 @@ async function emitTotp(
|
|
|
139
159
|
ExitCode.BadArgs,
|
|
140
160
|
);
|
|
141
161
|
}
|
|
142
|
-
emitValue(await readTotpSecret(item, opts), localOpts, opts);
|
|
162
|
+
emitValue(await readTotpSecret(item, opts, exactItemId), localOpts, opts);
|
|
143
163
|
return;
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
const totp = await readTotp(item, opts, {
|
|
167
|
+
exactItemId,
|
|
147
168
|
minSecondsRemaining: parseFreshSeconds(localOpts.fresh),
|
|
148
169
|
onWait: (seconds) =>
|
|
149
170
|
emitLog(`Waiting ${seconds}s for a fresh code…`, opts),
|
package/src/cli/commands/run.ts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { constants } from "node:os";
|
|
3
|
-
import { readFields } from "../../bw/fields.ts";
|
|
3
|
+
import { readFields, type FieldRequest } from "../../bw/fields.ts";
|
|
4
|
+
import { parseFieldReference, REFERENCE_PREFIX } from "../../bw/references.ts";
|
|
4
5
|
import { CliError, ExitCode } from "../errors.ts";
|
|
5
6
|
import { collect } from "../input.ts";
|
|
6
7
|
import { emitLog } from "../io.ts";
|
|
7
8
|
import { getGlobalOpts } from "../program.ts";
|
|
8
9
|
import type { GlobalOptions } from "../types.ts";
|
|
9
10
|
|
|
10
|
-
/** `NAME=<field>:<item>` — one secret
|
|
11
|
-
export interface EnvSpec {
|
|
11
|
+
/** `NAME=<field>:<item>` or `NAME=<ref>` — one secret for the child's environment. */
|
|
12
|
+
export interface EnvSpec extends FieldRequest {
|
|
12
13
|
name: string;
|
|
13
|
-
field: string;
|
|
14
|
-
item: string;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
18
17
|
|
|
19
|
-
const SPEC_HINT = "Expected --env NAME=<field>:<item>, e.g. --env DB_PASS=password:'Prod DB'";
|
|
18
|
+
const SPEC_HINT = "Expected --env NAME=<field>:<item> or NAME=<ref>, e.g. --env DB_PASS=password:'Prod DB'";
|
|
20
19
|
|
|
21
20
|
export function registerRun(program: Command): void {
|
|
22
21
|
program
|
|
@@ -25,7 +24,7 @@ export function registerRun(program: Command): void {
|
|
|
25
24
|
.argument("<command...>", "Command to run — use -- before it if it takes its own flags")
|
|
26
25
|
.option(
|
|
27
26
|
"--env <spec>",
|
|
28
|
-
"Inject a secret as NAME=<field>:<item> (repeatable)",
|
|
27
|
+
"Inject a secret as NAME=<field>:<item> or NAME=<ref> (repeatable)",
|
|
29
28
|
collect,
|
|
30
29
|
[],
|
|
31
30
|
)
|
|
@@ -71,7 +70,8 @@ export function parseEnvSpecs(raws: string[]): EnvSpec[] {
|
|
|
71
70
|
* Splits `NAME=<field>:<item>` on the first `=` and then the first `:`, so item
|
|
72
71
|
* names may contain colons (`password:http://my.router`) while field names may not.
|
|
73
72
|
* Fields resolve exactly as in `bwx get`: built-ins first, then card fields on a card
|
|
74
|
-
* item, otherwise custom fields.
|
|
73
|
+
* item, otherwise custom fields. A bwx:// reference supplies an exact ID and an
|
|
74
|
+
* explicit field kind, bypassing the legacy colon-separated grammar.
|
|
75
75
|
*/
|
|
76
76
|
export function parseEnvSpec(raw: string): EnvSpec {
|
|
77
77
|
const eq = raw.indexOf("=");
|
|
@@ -88,6 +88,9 @@ export function parseEnvSpec(raw: string): EnvSpec {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const ref = raw.slice(eq + 1);
|
|
91
|
+
if (ref.startsWith(REFERENCE_PREFIX)) {
|
|
92
|
+
return { name, ...parseFieldReference(ref) };
|
|
93
|
+
}
|
|
91
94
|
const colon = ref.indexOf(":");
|
|
92
95
|
if (colon <= 0 || colon === ref.length - 1) {
|
|
93
96
|
throw new CliError(`Invalid --env "${raw}". ${SPEC_HINT}`, ExitCode.BadArgs);
|
|
@@ -105,10 +108,7 @@ async function resolveSecrets(
|
|
|
105
108
|
specs: EnvSpec[],
|
|
106
109
|
opts: GlobalOptions,
|
|
107
110
|
): Promise<Record<string, string>> {
|
|
108
|
-
const values = await readFields(
|
|
109
|
-
specs.map((spec) => ({ field: spec.field, item: spec.item })),
|
|
110
|
-
opts,
|
|
111
|
-
);
|
|
111
|
+
const values = await readFields(specs, opts);
|
|
112
112
|
|
|
113
113
|
return Object.fromEntries(
|
|
114
114
|
specs.map((spec, index) => [spec.name, values[index]!]),
|
|
@@ -131,9 +131,10 @@ async function spawnChild(
|
|
|
131
131
|
stderr: "inherit",
|
|
132
132
|
env: { ...process.env, ...secrets },
|
|
133
133
|
});
|
|
134
|
-
} catch
|
|
134
|
+
} catch {
|
|
135
|
+
// Spawn diagnostics may quote the environment that already holds secrets.
|
|
135
136
|
throw new CliError(
|
|
136
|
-
|
|
137
|
+
"Failed to run requested command. Check the executable and environment.",
|
|
137
138
|
ExitCode.BadArgs,
|
|
138
139
|
);
|
|
139
140
|
}
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
import { runBwOrThrow } from "../../bw/runner.ts";
|
|
11
11
|
import { withSession } from "../../bw/session.ts";
|
|
12
12
|
import { resolveFolderFilter } from "../../bw/folders.ts";
|
|
13
|
+
import { withSafeVaultErrors } from "../../bw/safe-errors.ts";
|
|
14
|
+
import type { GlobalOptions } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
interface SearchOptions extends ItemListOptions {
|
|
17
|
+
folder?: string;
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
export function registerSearch(program: Command): void {
|
|
15
21
|
program
|
|
@@ -20,26 +26,37 @@ export function registerSearch(program: Command): void {
|
|
|
20
26
|
.option("--type <type>", "Filter by type: login|note|card|identity")
|
|
21
27
|
.option("--folder <name|id>", "Filter by folder name or ID ('none' for unfiled)")
|
|
22
28
|
.option("--limit <n>", "Limit results (0 for no limit)", parseLimit)
|
|
29
|
+
.option("--fields", "Discover available fields and reusable references (no values)")
|
|
23
30
|
.option("--full-items", "Emit complete item objects (includes secrets)")
|
|
24
31
|
.action(async function (
|
|
25
32
|
this: Command,
|
|
26
33
|
query: string,
|
|
27
|
-
localOpts:
|
|
34
|
+
localOpts: SearchOptions,
|
|
28
35
|
) {
|
|
29
36
|
const opts = getGlobalOpts(this);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (localOpts.folder) {
|
|
33
|
-
args.push("--folderid", await resolveFolderFilter(localOpts.folder, opts));
|
|
37
|
+
if (localOpts.fields && localOpts.fullItems) {
|
|
38
|
+
throw new CliError("--fields and --full-items cannot be combined.", ExitCode.BadArgs);
|
|
34
39
|
}
|
|
40
|
+
const search = () => searchItems(query, localOpts, opts);
|
|
41
|
+
if (localOpts.fields) await withSafeVaultErrors("Could not discover fields", search);
|
|
42
|
+
else await search();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
async function searchItems(
|
|
47
|
+
query: string,
|
|
48
|
+
localOpts: SearchOptions,
|
|
49
|
+
opts: GlobalOptions,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const args = ["list", "items", "--search", query];
|
|
52
|
+
if (localOpts.folder) {
|
|
53
|
+
args.push("--folderid", await resolveFolderFilter(localOpts.folder, opts));
|
|
54
|
+
}
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
const json = await withSession(opts, () => runBwOrThrow(args));
|
|
57
|
+
const filtered = filterItems(JSON.parse(json), localOpts);
|
|
58
|
+
if (filtered.total === 0) {
|
|
59
|
+
throw new CliError(`No items matching "${query}"`, ExitCode.NotFound);
|
|
60
|
+
}
|
|
61
|
+
writeItems(filtered, opts, localOpts);
|
|
45
62
|
}
|