@red-hat-developer-hub/e2e-test-utils 2.1.15 → 2.1.16

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.
Files changed (73) hide show
  1. package/dist/secrets/bitwarden.d.ts +43 -0
  2. package/dist/secrets/bitwarden.d.ts.map +1 -1
  3. package/dist/secrets/bitwarden.js +465 -53
  4. package/dist/secrets/cli.d.ts +48 -1
  5. package/dist/secrets/cli.d.ts.map +1 -1
  6. package/dist/secrets/cli.js +455 -28
  7. package/dist/secrets/command.d.ts +7 -0
  8. package/dist/secrets/command.d.ts.map +1 -1
  9. package/dist/secrets/command.js +103 -26
  10. package/dist/secrets/config.d.ts +11 -1
  11. package/dist/secrets/config.d.ts.map +1 -1
  12. package/dist/secrets/config.js +32 -1
  13. package/dist/secrets/environment-name.d.ts +2 -0
  14. package/dist/secrets/environment-name.d.ts.map +1 -0
  15. package/dist/secrets/environment-name.js +4 -0
  16. package/dist/secrets/environment.d.ts +10 -0
  17. package/dist/secrets/environment.d.ts.map +1 -1
  18. package/dist/secrets/environment.js +35 -4
  19. package/dist/secrets/exec.d.ts +9 -1
  20. package/dist/secrets/exec.d.ts.map +1 -1
  21. package/dist/secrets/exec.js +318 -38
  22. package/dist/secrets/gsm-wrapper.d.ts +42 -0
  23. package/dist/secrets/gsm-wrapper.d.ts.map +1 -0
  24. package/dist/secrets/gsm-wrapper.js +238 -0
  25. package/dist/secrets/gsm.d.ts +33 -0
  26. package/dist/secrets/gsm.d.ts.map +1 -0
  27. package/dist/secrets/gsm.js +172 -0
  28. package/dist/secrets/index.d.ts +9 -4
  29. package/dist/secrets/index.d.ts.map +1 -1
  30. package/dist/secrets/index.js +7 -2
  31. package/dist/secrets/lock.d.ts +3 -0
  32. package/dist/secrets/lock.d.ts.map +1 -0
  33. package/dist/secrets/lock.js +28 -0
  34. package/dist/secrets/mutation.d.ts +71 -0
  35. package/dist/secrets/mutation.d.ts.map +1 -0
  36. package/dist/secrets/mutation.js +210 -0
  37. package/dist/secrets/secret-input.d.ts +17 -0
  38. package/dist/secrets/secret-input.d.ts.map +1 -0
  39. package/dist/secrets/secret-input.js +40 -0
  40. package/dist/secrets/stream.d.ts +9 -0
  41. package/dist/secrets/stream.d.ts.map +1 -0
  42. package/dist/secrets/stream.js +211 -0
  43. package/dist/secrets/temporary-secret.d.ts +13 -0
  44. package/dist/secrets/temporary-secret.d.ts.map +1 -0
  45. package/dist/secrets/temporary-secret.js +87 -0
  46. package/dist/secrets/tests/bitwarden.test.js +640 -0
  47. package/dist/secrets/tests/cli.integration.test.js +131 -3
  48. package/dist/secrets/tests/cli.test.js +281 -6
  49. package/dist/secrets/tests/command.test.d.ts +2 -0
  50. package/dist/secrets/tests/command.test.d.ts.map +1 -0
  51. package/dist/secrets/tests/command.test.js +39 -0
  52. package/dist/secrets/tests/config.test.js +20 -1
  53. package/dist/secrets/tests/environment.test.js +145 -3
  54. package/dist/secrets/tests/exec.test.js +469 -2
  55. package/dist/secrets/tests/gsm-wrapper.test.d.ts +2 -0
  56. package/dist/secrets/tests/gsm-wrapper.test.d.ts.map +1 -0
  57. package/dist/secrets/tests/gsm-wrapper.test.js +166 -0
  58. package/dist/secrets/tests/gsm.test.d.ts +2 -0
  59. package/dist/secrets/tests/gsm.test.d.ts.map +1 -0
  60. package/dist/secrets/tests/gsm.test.js +181 -0
  61. package/dist/secrets/tests/mutation.test.d.ts +2 -0
  62. package/dist/secrets/tests/mutation.test.d.ts.map +1 -0
  63. package/dist/secrets/tests/mutation.test.js +390 -0
  64. package/dist/secrets/tests/secret-input.test.d.ts +2 -0
  65. package/dist/secrets/tests/secret-input.test.d.ts.map +1 -0
  66. package/dist/secrets/tests/secret-input.test.js +71 -0
  67. package/dist/secrets/tests/stream.test.d.ts +2 -0
  68. package/dist/secrets/tests/stream.test.d.ts.map +1 -0
  69. package/dist/secrets/tests/stream.test.js +88 -0
  70. package/dist/secrets/tests/temporary-secret.test.d.ts +2 -0
  71. package/dist/secrets/tests/temporary-secret.test.d.ts.map +1 -0
  72. package/dist/secrets/tests/temporary-secret.test.js +76 -0
  73. package/package.json +1 -1
@@ -1,11 +1,58 @@
1
1
  #!/usr/bin/env node
2
+ import { type MutationPlan } from "./mutation.js";
2
3
  export interface ExecCliArguments {
3
4
  command: "exec";
4
5
  profilePath: string;
5
6
  workspaces: string[];
6
7
  executable: string;
7
8
  args: string[];
9
+ streamSecrets?: boolean;
8
10
  }
9
- export declare function parseCliArguments(argv: readonly string[]): ExecCliArguments;
11
+ export type HelpTopic = "root" | "exec" | "create" | "update" | "delete" | "describe" | "list" | "gsm-login" | "gsm-clean";
12
+ export interface HelpCliArguments {
13
+ command: "help";
14
+ topic: HelpTopic;
15
+ }
16
+ interface MutationCliBase {
17
+ collection: string;
18
+ bitwardenPath: string;
19
+ dryRun: boolean;
20
+ gsmTimeoutMs?: number;
21
+ }
22
+ export interface CreateCliArguments extends MutationCliBase {
23
+ command: "create";
24
+ fromFile?: string;
25
+ fromStdin: boolean;
26
+ allowEmpty: boolean;
27
+ force: boolean;
28
+ }
29
+ export interface UpdateCliArguments extends MutationCliBase {
30
+ command: "update";
31
+ fromFile?: string;
32
+ fromStdin: boolean;
33
+ allowEmpty: boolean;
34
+ }
35
+ export interface DeleteCliArguments extends MutationCliBase {
36
+ command: "delete";
37
+ force: boolean;
38
+ }
39
+ export interface DescribeCliArguments {
40
+ command: "describe";
41
+ collection: string;
42
+ bitwardenPath: string;
43
+ output: "text" | "json";
44
+ }
45
+ export interface ListCliArguments {
46
+ command: "list";
47
+ collection?: string;
48
+ output: "text" | "json";
49
+ }
50
+ export type CliArguments = HelpCliArguments | ExecCliArguments | CreateCliArguments | UpdateCliArguments | DeleteCliArguments | DescribeCliArguments | ListCliArguments | {
51
+ command: "gsm-login" | "gsm-clean";
52
+ };
53
+ export declare function parseCliArguments(argv: readonly string[]): CliArguments;
54
+ export declare function getHelpText(topic: HelpTopic): string;
10
55
  export declare function main(argv?: readonly string[]): Promise<number>;
56
+ export declare function formatMutationResult(state: "dry-run" | "applied", plan: MutationPlan): string;
57
+ export {};
11
58
  //# sourceMappingURL=cli.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/secrets/cli.ts"],"names":[],"mappings":";AASA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAUD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAkD3E;AAED,wBAAsB,IAAI,CACxB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAC9C,OAAO,CAAC,MAAM,CAAC,CAwBjB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/secrets/cli.ts"],"names":[],"mappings":";AAeA,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,OAAO,EAAE,QAAQ,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,OAAO,EAAE,QAAQ,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,OAAO,EAAE,QAAQ,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,UAAU,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,YAAY,GACpB,gBAAgB,GAChB,gBAAgB,GAChB,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,oBAAoB,GACpB,gBAAgB,GAChB;IAAE,OAAO,EAAE,WAAW,GAAG,WAAW,CAAA;CAAE,CAAC;AA6F3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,YAAY,CAgBvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEpD;AA2SD,wBAAsB,IAAI,CACxB,IAAI,GAAE,SAAS,MAAM,EAA0B,GAC9C,OAAO,CAAC,MAAM,CAAC,CAoFjB;AAoBD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,IAAI,EAAE,YAAY,GACjB,MAAM,CASR"}
@@ -4,50 +4,177 @@ import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { resolve } from "node:path";
6
6
  import { executeCommand } from "./exec.js";
7
- import { parseProfile } from "./config.js";
8
- const HELP = `Usage:
9
- rhdh-e2e-secrets exec --profile <profile.json> [--workspace <name> ...] -- <command> [args...]
7
+ import { getCollectionMapping, gsmPathFromBitwardenPath, parseProfile, READABLE_COLLECTIONS, } from "./config.js";
8
+ import { GsmClient } from "./gsm.js";
9
+ import { GsmWrapper } from "./gsm-wrapper.js";
10
+ import { executeMutation, } from "./mutation.js";
11
+ import { BitwardenClient } from "./bitwarden.js";
12
+ import { MAX_TIMEOUT_MS } from "./command.js";
13
+ const HELP_TEXT = {
14
+ root: `Usage:
15
+ rhdh-e2e-secrets exec -p <profile.json> [-w <name> ...] [--stream-secrets] -- <command> [args...]
16
+ rhdh-e2e-secrets create -c <collection> <secret-path> (-f <file> | -i) [--allow-empty] [--force] [--dry-run]
17
+ rhdh-e2e-secrets update -c <collection> <secret-path> (-f <file> | -i) [--allow-empty] [--dry-run]
18
+ rhdh-e2e-secrets delete -c <collection> <secret-path> [--force] [--dry-run]
19
+ rhdh-e2e-secrets describe -c <collection> <secret-path> [-o text|json]
20
+ rhdh-e2e-secrets list [-c <collection>] [-o text|json]
21
+ rhdh-e2e-secrets gsm-login
22
+ rhdh-e2e-secrets gsm-clean
10
23
 
11
24
  Requirements:
12
- BW_SESSION must contain an already unlocked Bitwarden CLI session.
13
- The bw CLI must be installed and available on PATH.
14
- `;
25
+ BW_SESSION must contain an already unlocked Bitwarden CLI session for exec and create, update, and delete.
26
+ GSM commands use a cached copy of openshift/release hack/secret-manager.sh and require local gcloud authentication.
27
+ `,
28
+ exec: `Usage:
29
+ rhdh-e2e-secrets exec -p <profile.json> [-w <name> ...] [--stream-secrets] -- <command> [args...]
30
+
31
+ Options:
32
+ -p, --profile <file> Secret profile JSON file (required)
33
+ -w, --workspace <name> Limit execution to a workspace; repeatable
34
+ --stream-secrets Stream selected values to child file descriptor 3
35
+ -h, --help Show this help
36
+ `,
37
+ create: `Usage:
38
+ rhdh-e2e-secrets create -c <collection> <secret-path> (-f <file> | -i) [options]
39
+
40
+ Options:
41
+ -c, --collection <name> Paired secret collection (required)
42
+ -f, --from-file <file> Read an attachment-backed secret from a file
43
+ -i, --from-stdin Read a note-backed secret from stdin
44
+ --allow-empty Allow an empty secret value
45
+ --force Reconcile an existing or partial target
46
+ --dry-run Validate and print the plan without writing
47
+ --gsm-timeout-seconds <n>
48
+ GSM operation timeout
49
+ -h, --help Show this help
50
+ `,
51
+ update: `Usage:
52
+ rhdh-e2e-secrets update -c <collection> <secret-path> (-f <file> | -i) [options]
53
+
54
+ Options:
55
+ -c, --collection <name> Paired secret collection (required)
56
+ -f, --from-file <file> Read an attachment-backed secret from a file
57
+ -i, --from-stdin Read a note-backed secret from stdin
58
+ --allow-empty Allow an empty secret value
59
+ --dry-run Validate and print the plan without writing
60
+ --gsm-timeout-seconds <n>
61
+ GSM operation timeout
62
+ -h, --help Show this help
63
+ `,
64
+ delete: `Usage:
65
+ rhdh-e2e-secrets delete -c <collection> <secret-path> [options]
66
+
67
+ Options:
68
+ -c, --collection <name> Paired secret collection (required)
69
+ --force Reconcile an already partial target
70
+ --dry-run Validate and print the plan without writing
71
+ --gsm-timeout-seconds <n>
72
+ GSM operation timeout
73
+ -h, --help Show this help
74
+ `,
75
+ describe: `Usage:
76
+ rhdh-e2e-secrets describe -c <collection> <secret-path> [-o text|json]
77
+
78
+ Options:
79
+ -c, --collection <name> Paired secret collection (required)
80
+ -o, --output <text|json> Output format (default: text)
81
+ -h, --help Show this help
82
+ `,
83
+ list: `Usage:
84
+ rhdh-e2e-secrets list [-c <collection>] [-o text|json]
85
+
86
+ Options:
87
+ -c, --collection <name> List paths in one paired collection
88
+ -o, --output <text|json> Output format (default: text)
89
+ -h, --help Show this help
90
+ `,
91
+ ["gsm-login"]: `Usage:
92
+ rhdh-e2e-secrets gsm-login
93
+
94
+ Authenticate the cached GSM wrapper.
95
+ `,
96
+ ["gsm-clean"]: `Usage:
97
+ rhdh-e2e-secrets gsm-clean
98
+
99
+ Remove cached GSM authentication.
100
+ `,
101
+ };
102
+ const MUTATION_COMMANDS = ["create", "update", "delete"];
15
103
  export function parseCliArguments(argv) {
16
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
17
- throw new Error(HELP);
18
- }
19
- if (argv[0] !== "exec") {
20
- throw new Error(`Unsupported command: ${argv[0]}`);
104
+ if (argv.length === 0)
105
+ return { command: "help", topic: "root" };
106
+ if (argv[0] === "--help" || argv[0] === "-h")
107
+ return { command: "help", topic: "root" };
108
+ const helpTopic = findHelpTopic(argv);
109
+ if (helpTopic)
110
+ return { command: "help", topic: helpTopic };
111
+ if (argv[0] === "exec")
112
+ return parseExecArguments(argv);
113
+ if (isMutationCommand(argv[0]))
114
+ return parseMutationArguments(argv);
115
+ if (argv[0] === "describe")
116
+ return parseDescribeArguments(argv);
117
+ if (argv[0] === "list")
118
+ return parseListArguments(argv);
119
+ if (argv[0] === "gsm-login" || argv[0] === "gsm-clean") {
120
+ if (argv.length !== 1)
121
+ throw new Error(`${argv[0]} does not accept options`);
122
+ return { command: argv[0] };
21
123
  }
124
+ throw new Error(`Unsupported command: ${argv[0]}`);
125
+ }
126
+ export function getHelpText(topic) {
127
+ return HELP_TEXT[topic];
128
+ }
129
+ function findHelpTopic(argv) {
130
+ const command = argv[0];
131
+ if (!isHelpTopic(command))
132
+ return undefined;
133
+ const delimiter = command === "exec" ? argv.indexOf("--") : -1;
134
+ const options = argv.slice(1, delimiter === -1 ? argv.length : delimiter);
135
+ return options.includes("--help") || options.includes("-h")
136
+ ? command
137
+ : undefined;
138
+ }
139
+ function parseExecArguments(argv) {
22
140
  const delimiter = argv.indexOf("--");
23
- if (delimiter === -1) {
141
+ if (delimiter === -1)
24
142
  throw new Error("A command after -- is required");
25
- }
26
143
  const options = argv.slice(1, delimiter);
27
144
  const command = argv[delimiter + 1];
28
145
  const args = argv.slice(delimiter + 2);
29
146
  let profilePath;
30
147
  const workspaces = [];
148
+ let streamSecrets = false;
31
149
  for (let index = 0; index < options.length; index++) {
32
150
  const option = options[index];
33
- if (option === "--profile" || option === "--workspace") {
151
+ if (option === "--stream-secrets") {
152
+ streamSecrets = true;
153
+ continue;
154
+ }
155
+ if (option === "--profile" ||
156
+ option === "-p" ||
157
+ option === "--workspace" ||
158
+ option === "-w") {
34
159
  const value = options[++index];
35
160
  if (!value)
36
161
  throw new Error(`${option} requires a value`);
37
- if (option === "--profile")
162
+ if (option === "--profile" || option === "-p")
38
163
  profilePath = value;
39
164
  else
40
165
  workspaces.push(value);
41
166
  continue;
42
167
  }
43
- if (option.startsWith("--profile=")) {
44
- profilePath = option.slice("--profile=".length);
168
+ if (option.startsWith("--profile=") || option.startsWith("-p=")) {
169
+ const prefix = option.startsWith("--profile=") ? "--profile=" : "-p=";
170
+ profilePath = option.slice(prefix.length);
45
171
  if (!profilePath)
46
172
  throw new Error("--profile requires a value");
47
173
  continue;
48
174
  }
49
- if (option.startsWith("--workspace=")) {
50
- const workspace = option.slice("--workspace=".length);
175
+ if (option.startsWith("--workspace=") || option.startsWith("-w=")) {
176
+ const prefix = option.startsWith("--workspace=") ? "--workspace=" : "-w=";
177
+ const workspace = option.slice(prefix.length);
51
178
  if (!workspace)
52
179
  throw new Error("--workspace requires a value");
53
180
  workspaces.push(workspace);
@@ -65,29 +192,329 @@ export function parseCliArguments(argv) {
65
192
  workspaces,
66
193
  executable: command,
67
194
  args,
195
+ ...(streamSecrets ? { streamSecrets: true } : {}),
196
+ };
197
+ }
198
+ function parseMutationArguments(argv) {
199
+ const command = argv[0];
200
+ let collection;
201
+ let bitwardenPath;
202
+ let fromFile;
203
+ let fromStdin = false;
204
+ let allowEmpty = false;
205
+ let force = false;
206
+ let dryRun = false;
207
+ let gsmTimeoutMs;
208
+ let pathSeen = false;
209
+ const seenOptions = new Set();
210
+ const markOption = (name) => {
211
+ if (seenOptions.has(name))
212
+ throw new Error(`Duplicate option: ${name}`);
213
+ seenOptions.add(name);
214
+ };
215
+ for (let index = 1; index < argv.length; index++) {
216
+ const option = argv[index];
217
+ if (!option.startsWith("-")) {
218
+ if (pathSeen)
219
+ throw new Error("Only one secret path is allowed");
220
+ pathSeen = true;
221
+ bitwardenPath = option;
222
+ continue;
223
+ }
224
+ if (option === "--from-stdin" || option === "-i") {
225
+ markOption("--from-stdin");
226
+ fromStdin = true;
227
+ continue;
228
+ }
229
+ if (option === "--allow-empty" ||
230
+ option === "--force" ||
231
+ option === "--dry-run") {
232
+ markOption(option);
233
+ if (option === "--allow-empty")
234
+ allowEmpty = true;
235
+ else if (option === "--force")
236
+ force = true;
237
+ else
238
+ dryRun = true;
239
+ continue;
240
+ }
241
+ const valueOption = readMutationValueOption(option, argv, index);
242
+ if (valueOption) {
243
+ markOption(valueOption.name);
244
+ index = valueOption.nextIndex;
245
+ if (valueOption.name === "--collection")
246
+ collection = valueOption.value;
247
+ else if (valueOption.name === "--from-file")
248
+ fromFile = valueOption.value;
249
+ else if (valueOption.name === "--gsm-timeout-seconds") {
250
+ const seconds = Number(valueOption.value);
251
+ if (!Number.isSafeInteger(seconds) ||
252
+ seconds <= 0 ||
253
+ seconds * 1000 > MAX_TIMEOUT_MS) {
254
+ throw new Error(`--gsm-timeout-seconds must be a positive integer no greater than ${Math.floor(MAX_TIMEOUT_MS / 1000)}`);
255
+ }
256
+ gsmTimeoutMs = seconds * 1000;
257
+ }
258
+ continue;
259
+ }
260
+ throw new Error(`Unknown option: ${option}`);
261
+ }
262
+ if (!collection)
263
+ throw new Error("--collection is required");
264
+ if (!bitwardenPath)
265
+ throw new Error("A secret path is required");
266
+ if (command === "delete") {
267
+ if (fromFile !== undefined || fromStdin || allowEmpty) {
268
+ throw new Error("delete does not accept input options");
269
+ }
270
+ return {
271
+ command,
272
+ collection,
273
+ bitwardenPath,
274
+ force,
275
+ dryRun,
276
+ gsmTimeoutMs,
277
+ };
278
+ }
279
+ if (force && command === "update") {
280
+ throw new Error("--force is not supported for update");
281
+ }
282
+ if (Number(fromFile !== undefined) + Number(fromStdin) !== 1) {
283
+ throw new Error("Exactly one of --from-file or --from-stdin is required");
284
+ }
285
+ return {
286
+ command,
287
+ collection,
288
+ bitwardenPath,
289
+ fromFile,
290
+ fromStdin,
291
+ allowEmpty,
292
+ ...(command === "create" ? { force } : {}),
293
+ dryRun,
294
+ gsmTimeoutMs,
295
+ };
296
+ }
297
+ function readMutationValueOption(option, argv, index) {
298
+ const aliases = new Map([
299
+ ["-c", "--collection"],
300
+ ["-f", "--from-file"],
301
+ ]);
302
+ const names = [
303
+ "--collection",
304
+ "--from-file",
305
+ "--gsm-timeout-seconds",
306
+ ];
307
+ const name = aliases.get(option) ??
308
+ names.find((candidate) => option === candidate || option.startsWith(`${candidate}=`));
309
+ if (!name)
310
+ return undefined;
311
+ const inline = option.startsWith(`${name}=`)
312
+ ? option.slice(name.length + 1)
313
+ : argv[index + 1];
314
+ if (!inline)
315
+ throw new Error(`${name} requires a value`);
316
+ return {
317
+ name,
318
+ value: inline,
319
+ nextIndex: option === name || option === "-c" || option === "-f" ? index + 1 : index,
320
+ };
321
+ }
322
+ function parseDescribeArguments(argv) {
323
+ const parsed = parseReadArguments(argv, true);
324
+ return {
325
+ command: "describe",
326
+ collection: parsed.collection,
327
+ bitwardenPath: parsed.bitwardenPath,
328
+ output: parsed.output,
329
+ };
330
+ }
331
+ function parseListArguments(argv) {
332
+ const parsed = parseReadArguments(argv, false);
333
+ return {
334
+ command: "list",
335
+ ...(parsed.collection === undefined
336
+ ? {}
337
+ : { collection: parsed.collection }),
338
+ output: parsed.output,
339
+ };
340
+ }
341
+ function parseReadArguments(argv, pathRequired) {
342
+ let collection;
343
+ let bitwardenPath;
344
+ let output = "text";
345
+ const seen = new Set();
346
+ for (let index = 1; index < argv.length; index++) {
347
+ const option = argv[index];
348
+ if (!option.startsWith("-")) {
349
+ if (bitwardenPath)
350
+ throw new Error("Only one secret path is allowed");
351
+ bitwardenPath = option;
352
+ continue;
353
+ }
354
+ const value = readReadValueOption(option, argv, index);
355
+ if (value) {
356
+ if (seen.has(value.name))
357
+ throw new Error(`Duplicate option: ${value.name}`);
358
+ seen.add(value.name);
359
+ index = value.nextIndex;
360
+ if (value.name === "--collection")
361
+ collection = value.value;
362
+ else {
363
+ const normalized = value.value.toLowerCase();
364
+ if (normalized !== "text" && normalized !== "json") {
365
+ throw new Error("--output must be text or json");
366
+ }
367
+ output = normalized;
368
+ }
369
+ continue;
370
+ }
371
+ throw new Error(`Unknown option: ${option}`);
372
+ }
373
+ if (pathRequired && !bitwardenPath)
374
+ throw new Error("A secret path is required");
375
+ if (pathRequired && !collection)
376
+ throw new Error("--collection is required");
377
+ if (!pathRequired && bitwardenPath) {
378
+ throw new Error("list does not accept a secret path");
379
+ }
380
+ return { collection, bitwardenPath, output };
381
+ }
382
+ function readReadValueOption(option, argv, index) {
383
+ const name = option === "-c"
384
+ ? "--collection"
385
+ : option === "-o"
386
+ ? "--output"
387
+ : ["--collection", "--output"].find((candidate) => option === candidate || option.startsWith(`${candidate}=`));
388
+ if (!name)
389
+ return undefined;
390
+ const inline = option.startsWith(`${name}=`)
391
+ ? option.slice(name.length + 1)
392
+ : argv[index + 1];
393
+ if (!inline)
394
+ throw new Error(`${name} requires a value`);
395
+ return {
396
+ name,
397
+ value: inline,
398
+ nextIndex: option === name || option === "-c" || option === "-o" ? index + 1 : index,
68
399
  };
69
400
  }
70
401
  export async function main(argv = process.argv.slice(2)) {
71
402
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
72
- console.log(HELP);
403
+ console.log(getHelpText("root"));
73
404
  return 0;
74
405
  }
75
406
  try {
76
407
  const parsed = parseCliArguments(argv);
77
- const profileValue = JSON.parse(await readFile(resolve(parsed.profilePath), "utf8"));
78
- const profile = parseProfile(profileValue);
79
- return await executeCommand({
80
- profile,
81
- workspaces: parsed.workspaces,
82
- command: parsed.executable,
83
- args: parsed.args,
408
+ if (parsed.command === "help") {
409
+ console.log(getHelpText(parsed.topic));
410
+ return 0;
411
+ }
412
+ if (parsed.command === "exec") {
413
+ const profileValue = JSON.parse(await readFile(resolve(parsed.profilePath), "utf8"));
414
+ return await executeCommand({
415
+ profile: parseProfile(profileValue),
416
+ workspaces: parsed.workspaces,
417
+ command: parsed.executable,
418
+ args: parsed.args,
419
+ streamSecrets: parsed.streamSecrets,
420
+ });
421
+ }
422
+ if (parsed.command === "gsm-login" || parsed.command === "gsm-clean") {
423
+ const wrapper = new GsmWrapper();
424
+ const result = parsed.command === "gsm-login"
425
+ ? await wrapper.login()
426
+ : await wrapper.clean();
427
+ if (result.status !== 0)
428
+ throw new Error(`${parsed.command} failed`);
429
+ return 0;
430
+ }
431
+ const gsm = new GsmClient();
432
+ if (parsed.command === "describe") {
433
+ const mapping = getCollectionMapping(parsed.collection);
434
+ const metadata = await gsm.describe(mapping.gsmCollection, gsmPathFromBitwardenPath(parsed.bitwardenPath));
435
+ printOutput(metadata, parsed.output);
436
+ return 0;
437
+ }
438
+ if (parsed.command === "list") {
439
+ if (parsed.collection === undefined) {
440
+ printOutput([...READABLE_COLLECTIONS], parsed.output);
441
+ }
442
+ else {
443
+ const mapping = getCollectionMapping(parsed.collection);
444
+ const paths = await gsm.list(mapping.gsmCollection);
445
+ printOutput(paths, parsed.output);
446
+ }
447
+ return 0;
448
+ }
449
+ if (!isMutationCommand(parsed.command)) {
450
+ throw new Error(`Unsupported command: ${parsed.command}`);
451
+ }
452
+ const mutation = parsed;
453
+ const result = await executeMutation({
454
+ command: mutation.command,
455
+ collection: mutation.collection,
456
+ bitwardenPath: mutation.bitwardenPath,
457
+ fromFile: "fromFile" in mutation ? mutation.fromFile : undefined,
458
+ fromStdin: "fromStdin" in mutation ? mutation.fromStdin : undefined,
459
+ stdin: process.stdin,
460
+ allowEmpty: "allowEmpty" in mutation ? mutation.allowEmpty : undefined,
461
+ force: "force" in mutation ? mutation.force : undefined,
462
+ dryRun: mutation.dryRun,
463
+ gsmTimeoutMs: mutation.gsmTimeoutMs,
464
+ bitwarden: new BitwardenClient(),
465
+ gsm,
84
466
  });
467
+ console.log(formatMutationResult(result.state, result.plan));
468
+ return 0;
85
469
  }
86
470
  catch (error) {
87
- console.error(error instanceof Error ? error.message : "Secret execution failed");
471
+ console.error(error instanceof Error ? error.message : "Secret operation failed");
88
472
  return 1;
89
473
  }
90
474
  }
475
+ function printOutput(value, output) {
476
+ if (output === "json") {
477
+ console.log(JSON.stringify(value, null, 2));
478
+ return;
479
+ }
480
+ if (Array.isArray(value)) {
481
+ for (const entry of value)
482
+ console.log(String(entry));
483
+ return;
484
+ }
485
+ if (value && typeof value === "object") {
486
+ for (const [key, entry] of Object.entries(value)) {
487
+ console.log(`${key}: ${String(entry)}`);
488
+ }
489
+ return;
490
+ }
491
+ console.log(String(value));
492
+ }
493
+ export function formatMutationResult(state, plan) {
494
+ const lines = [
495
+ `${state === "dry-run" ? "Dry-run" : "Applied"}: ${plan.command} ${plan.collection}/${plan.bitwardenPath}`,
496
+ `Bitwarden: ${plan.bitwardenAction} ${plan.bitwardenCollection}/${plan.bitwardenPath}${plan.storage ? ` (${plan.storage})` : ""}`,
497
+ `GSM: ${plan.gsmAction} ${plan.gsmCollection}/${plan.gsmPath}`,
498
+ ];
499
+ if (plan.byteLength !== undefined)
500
+ lines.push(`Input: ${plan.byteLength} bytes`);
501
+ return lines.join("\n");
502
+ }
503
+ function isMutationCommand(value) {
504
+ return MUTATION_COMMANDS.includes(value);
505
+ }
506
+ function isHelpTopic(value) {
507
+ return [
508
+ "exec",
509
+ "create",
510
+ "update",
511
+ "delete",
512
+ "describe",
513
+ "list",
514
+ "gsm-login",
515
+ "gsm-clean",
516
+ ].includes(value);
517
+ }
91
518
  if (process.argv[1] &&
92
519
  realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
93
520
  void main().then((exitCode) => {
@@ -3,13 +3,20 @@ export interface CommandResult {
3
3
  stdout: string;
4
4
  stderr: string;
5
5
  error?: Error;
6
+ signal?: NodeJS.Signals;
7
+ timedOut?: boolean;
6
8
  }
7
9
  export interface CommandOptions {
8
10
  cwd?: string;
9
11
  env?: NodeJS.ProcessEnv;
10
12
  input?: string;
11
13
  stdio?: "pipe" | "inherit";
14
+ tty?: boolean;
15
+ timeoutMs?: number;
12
16
  }
17
+ export declare const MAX_TIMEOUT_MS = 2147483647;
13
18
  export type CommandRunner = (command: string, args: readonly string[], options?: CommandOptions) => Promise<CommandResult>;
19
+ export declare function shouldDetachCommand(options?: Pick<CommandOptions, "tty">): boolean;
14
20
  export declare const runCommand: CommandRunner;
21
+ export declare function assertInteractiveTerminal(): Promise<void>;
15
22
  //# sourceMappingURL=command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/secrets/command.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,eAAO,MAAM,UAAU,EAAE,aA6BrB,CAAC"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/secrets/command.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,cAAc,aAAgB,CAAC;AAE5C,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,aAAa,CAAC,CAAC;AAE5B,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,IAAI,CAAC,cAAc,EAAE,KAAK,CAAM,GACxC,OAAO,CAET;AAED,eAAO,MAAM,UAAU,EAAE,aAuGxB,CAAC;AAEF,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/D"}