@barekey/cli 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,496 @@
1
+ import { writeFile } from "node:fs/promises";
2
+
3
+ import { cancel, confirm, isCancel } from "@clack/prompts";
4
+ import { BarekeyClient } from "@barekey/sdk";
5
+ import { Command } from "commander";
6
+ import pc from "picocolors";
7
+
8
+ import { createCliAuthProvider } from "../auth-provider.js";
9
+ import {
10
+ addTargetOptions,
11
+ dotenvEscape,
12
+ parseChance,
13
+ requireLocalSession,
14
+ resolveTarget,
15
+ toJsonOutput,
16
+ type EnvTargetOptions,
17
+ } from "../command-utils.js";
18
+ import { postJson } from "../http.js";
19
+
20
+ function createEnvClient(input: {
21
+ organization: string | undefined;
22
+ project: string;
23
+ environment: string;
24
+ }): BarekeyClient {
25
+ const organization = input.organization?.trim();
26
+ if (!organization) {
27
+ throw new Error("Organization slug is required.");
28
+ }
29
+
30
+ return new BarekeyClient({
31
+ organization,
32
+ project: input.project,
33
+ environment: input.environment,
34
+ });
35
+ }
36
+
37
+ async function runEnvGet(
38
+ name: string,
39
+ options: EnvTargetOptions & {
40
+ seed?: string;
41
+ key?: string;
42
+ json?: boolean;
43
+ },
44
+ ): Promise<void> {
45
+ const local = await requireLocalSession();
46
+ const target = await resolveTarget(options, local);
47
+ const client = createEnvClient({
48
+ organization: target.orgSlug ?? local.credentials.orgSlug,
49
+ project: target.projectSlug,
50
+ environment: target.stageSlug,
51
+ });
52
+ const value = await client.get(name, {
53
+ seed: options.seed,
54
+ key: options.key,
55
+ });
56
+
57
+ if (options.json) {
58
+ toJsonOutput(true, {
59
+ name,
60
+ value,
61
+ });
62
+ return;
63
+ }
64
+
65
+ console.log(String(value));
66
+ }
67
+
68
+ async function runEnvGetMany(
69
+ options: EnvTargetOptions & {
70
+ names: string;
71
+ seed?: string;
72
+ key?: string;
73
+ json?: boolean;
74
+ },
75
+ ): Promise<void> {
76
+ const local = await requireLocalSession();
77
+ const target = await resolveTarget(options, local);
78
+ const client = createEnvClient({
79
+ organization: target.orgSlug ?? local.credentials.orgSlug,
80
+ project: target.projectSlug,
81
+ environment: target.stageSlug,
82
+ });
83
+
84
+ const names = options.names
85
+ .split(",")
86
+ .map((value) => value.trim())
87
+ .filter((value) => value.length > 0);
88
+
89
+ const resolved = await Promise.all(
90
+ names.map(async (resolvedName) => ({
91
+ name: resolvedName,
92
+ value: await client.get(resolvedName, {
93
+ seed: options.seed,
94
+ key: options.key,
95
+ }),
96
+ })),
97
+ );
98
+
99
+ if (options.json) {
100
+ toJsonOutput(true, resolved);
101
+ return;
102
+ }
103
+
104
+ for (const value of resolved.sort((left, right) => left.name.localeCompare(right.name))) {
105
+ if (value) {
106
+ console.log(`${value.name}=${String(value.value)}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ async function runEnvList(options: EnvTargetOptions & { json?: boolean }): Promise<void> {
112
+ const local = await requireLocalSession();
113
+ const target = await resolveTarget(options, local);
114
+ const authProvider = createCliAuthProvider();
115
+ const accessToken = await authProvider.getAccessToken();
116
+
117
+ const response = await postJson<{
118
+ variables: Array<{
119
+ name: string;
120
+ kind: "secret" | "ab_roll" | "rollout";
121
+ declaredType: "string" | "boolean" | "int64" | "float" | "date" | "json";
122
+ createdAtMs: number;
123
+ updatedAtMs: number;
124
+ chance: number | null;
125
+ rolloutFunction: "linear" | null;
126
+ rolloutMilestones: Array<{ at: string; percentage: number }> | null;
127
+ }>;
128
+ }>({
129
+ baseUrl: local.baseUrl,
130
+ path: "/v1/env/list",
131
+ accessToken,
132
+ payload: {
133
+ orgSlug: target.orgSlug,
134
+ projectSlug: target.projectSlug,
135
+ stageSlug: target.stageSlug,
136
+ },
137
+ });
138
+
139
+ if (options.json) {
140
+ toJsonOutput(true, response.variables);
141
+ return;
142
+ }
143
+
144
+ if (response.variables.length === 0) {
145
+ console.log("No variables found.");
146
+ return;
147
+ }
148
+
149
+ for (const row of response.variables) {
150
+ const chanceSuffix = row.kind === "ab_roll" ? ` chance=${row.chance ?? 0}` : "";
151
+ const rolloutSuffix =
152
+ row.kind === "rollout"
153
+ ? ` ${pc.dim(
154
+ `${row.rolloutFunction ?? "linear"}(${row.rolloutMilestones?.length ?? 0} milestones)`,
155
+ )}`
156
+ : "";
157
+ console.log(
158
+ `${row.name} ${pc.dim(row.kind)} ${pc.dim(row.declaredType)}${chanceSuffix}${rolloutSuffix}`,
159
+ );
160
+ }
161
+ }
162
+
163
+ async function runEnvWrite(
164
+ operation: "create_only" | "upsert",
165
+ name: string,
166
+ value: string,
167
+ options: EnvTargetOptions & {
168
+ ab?: string;
169
+ chance?: string;
170
+ type?: "string" | "boolean" | "int64" | "float" | "date" | "json";
171
+ json?: boolean;
172
+ },
173
+ ): Promise<void> {
174
+ const local = await requireLocalSession();
175
+ const target = await resolveTarget(options, local);
176
+ const authProvider = createCliAuthProvider();
177
+ const accessToken = await authProvider.getAccessToken();
178
+
179
+ const entry =
180
+ options.ab !== undefined
181
+ ? {
182
+ name,
183
+ kind: "ab_roll" as const,
184
+ declaredType: options.type ?? "string",
185
+ valueA: value,
186
+ valueB: options.ab,
187
+ chance: parseChance(options.chance),
188
+ }
189
+ : {
190
+ name,
191
+ kind: "secret" as const,
192
+ declaredType: options.type ?? "string",
193
+ value,
194
+ };
195
+
196
+ const result = await postJson<{
197
+ createdCount: number;
198
+ updatedCount: number;
199
+ deletedCount: number;
200
+ }>({
201
+ baseUrl: local.baseUrl,
202
+ path: "/v1/env/write",
203
+ accessToken,
204
+ payload: {
205
+ orgSlug: target.orgSlug,
206
+ projectSlug: target.projectSlug,
207
+ stageSlug: target.stageSlug,
208
+ mode: operation,
209
+ entries: [entry],
210
+ deletes: [],
211
+ },
212
+ });
213
+
214
+ if (options.json) {
215
+ toJsonOutput(true, result);
216
+ return;
217
+ }
218
+
219
+ console.log(
220
+ `Created: ${result.createdCount}, Updated: ${result.updatedCount}, Deleted: ${result.deletedCount}`,
221
+ );
222
+ }
223
+
224
+ async function runEnvDelete(
225
+ name: string,
226
+ options: EnvTargetOptions & {
227
+ yes?: boolean;
228
+ ignoreMissing?: boolean;
229
+ json?: boolean;
230
+ },
231
+ ): Promise<void> {
232
+ const local = await requireLocalSession();
233
+ const target = await resolveTarget(options, local);
234
+ const authProvider = createCliAuthProvider();
235
+ const accessToken = await authProvider.getAccessToken();
236
+
237
+ if (!options.ignoreMissing) {
238
+ const listed = await postJson<{
239
+ variables: Array<{
240
+ name: string;
241
+ }>;
242
+ }>({
243
+ baseUrl: local.baseUrl,
244
+ path: "/v1/env/list",
245
+ accessToken,
246
+ payload: {
247
+ orgSlug: target.orgSlug,
248
+ projectSlug: target.projectSlug,
249
+ stageSlug: target.stageSlug,
250
+ },
251
+ });
252
+ const exists = listed.variables.some((row) => row.name === name);
253
+ if (!exists) {
254
+ throw new Error(`Variable ${name} was not found in this stage.`);
255
+ }
256
+ }
257
+
258
+ if (!options.yes) {
259
+ if (!process.stdout.isTTY) {
260
+ throw new Error("Deletion requires --yes in non-interactive mode.");
261
+ }
262
+ const confirmed = await confirm({
263
+ message: `Delete variable ${name}?`,
264
+ initialValue: false,
265
+ });
266
+ if (isCancel(confirmed)) {
267
+ cancel("Delete canceled.");
268
+ return;
269
+ }
270
+ if (!confirmed) {
271
+ throw new Error("Delete canceled.");
272
+ }
273
+ }
274
+
275
+ const result = await postJson<{
276
+ createdCount: number;
277
+ updatedCount: number;
278
+ deletedCount: number;
279
+ }>({
280
+ baseUrl: local.baseUrl,
281
+ path: "/v1/env/write",
282
+ accessToken,
283
+ payload: {
284
+ orgSlug: target.orgSlug,
285
+ projectSlug: target.projectSlug,
286
+ stageSlug: target.stageSlug,
287
+ mode: "upsert",
288
+ entries: [],
289
+ deletes: [name],
290
+ },
291
+ });
292
+
293
+ if (options.json) {
294
+ toJsonOutput(true, result);
295
+ return;
296
+ }
297
+
298
+ console.log(`Deleted: ${result.deletedCount}`);
299
+ }
300
+
301
+ async function runEnvPull(
302
+ options: EnvTargetOptions & {
303
+ format?: "dotenv" | "json";
304
+ out?: string;
305
+ seed?: string;
306
+ key?: string;
307
+ redact?: boolean;
308
+ },
309
+ ): Promise<void> {
310
+ const local = await requireLocalSession();
311
+ const target = await resolveTarget(options, local);
312
+ const authProvider = createCliAuthProvider();
313
+ const accessToken = await authProvider.getAccessToken();
314
+
315
+ const response = await postJson<{
316
+ values: Array<{
317
+ name: string;
318
+ kind: "secret" | "ab_roll";
319
+ declaredType: "string" | "boolean" | "int64" | "float" | "date" | "json";
320
+ value: string;
321
+ }>;
322
+ byName: Record<string, string>;
323
+ }>({
324
+ baseUrl: local.baseUrl,
325
+ path: "/v1/env/pull",
326
+ accessToken,
327
+ payload: {
328
+ orgSlug: target.orgSlug,
329
+ projectSlug: target.projectSlug,
330
+ stageSlug: target.stageSlug,
331
+ seed: options.seed,
332
+ key: options.key,
333
+ },
334
+ });
335
+
336
+ const format = options.format ?? "dotenv";
337
+ const sortedKeys = Object.keys(response.byName).sort((left, right) => left.localeCompare(right));
338
+ const output =
339
+ format === "json"
340
+ ? `${JSON.stringify(response.byName, null, 2)}\n`
341
+ : `${sortedKeys.map((keyName) => `${keyName}=${dotenvEscape(response.byName[keyName] ?? "")}`).join("\n")}\n`;
342
+
343
+ if (options.out) {
344
+ await writeFile(options.out, output, "utf8");
345
+ console.log(`Wrote ${options.out}`);
346
+ return;
347
+ }
348
+
349
+ if (options.redact && !process.stdout.isTTY) {
350
+ console.log(`Pulled ${response.values.length} variables.`);
351
+ return;
352
+ }
353
+
354
+ process.stdout.write(output);
355
+ }
356
+
357
+ export function registerEnvCommands(program: Command): void {
358
+ const env = program.command("env").description("Environment variable operations");
359
+
360
+ addTargetOptions(
361
+ env
362
+ .command("get")
363
+ .description("Evaluate one variable")
364
+ .argument("<name>", "Variable name")
365
+ .option("--seed <value>", "Deterministic seed")
366
+ .option("--key <value>", "Deterministic key")
367
+ .option("--json", "Machine-readable output", false),
368
+ ).action(
369
+ async (
370
+ name: string,
371
+ options: EnvTargetOptions & { seed?: string; key?: string; json?: boolean },
372
+ ) => {
373
+ await runEnvGet(name, options);
374
+ },
375
+ );
376
+
377
+ addTargetOptions(
378
+ env
379
+ .command("get-many")
380
+ .description("Evaluate a batch of variables")
381
+ .requiredOption("--names <csv>", "Comma-separated variable names")
382
+ .option("--seed <value>", "Deterministic seed")
383
+ .option("--key <value>", "Deterministic key")
384
+ .option("--json", "Machine-readable output", false),
385
+ ).action(
386
+ async (
387
+ options: EnvTargetOptions & {
388
+ names: string;
389
+ seed?: string;
390
+ key?: string;
391
+ json?: boolean;
392
+ },
393
+ ) => {
394
+ await runEnvGetMany(options);
395
+ },
396
+ );
397
+
398
+ addTargetOptions(
399
+ env
400
+ .command("list")
401
+ .description("List variables for a project stage")
402
+ .option("--json", "Machine-readable output", false),
403
+ ).action(async (options: EnvTargetOptions & { json?: boolean }) => {
404
+ await runEnvList(options);
405
+ });
406
+
407
+ addTargetOptions(
408
+ env
409
+ .command("new")
410
+ .description("Create one variable")
411
+ .argument("<name>", "Variable name")
412
+ .argument("<value>", "Variable value")
413
+ .option("--ab <value-b>", "Second value for ab_roll")
414
+ .option("--chance <number>", "A-branch probability between 0 and 1")
415
+ .option("--type <type>", "Declared value type", "string")
416
+ .option("--json", "Machine-readable output", false),
417
+ ).action(
418
+ async (
419
+ name: string,
420
+ value: string,
421
+ options: EnvTargetOptions & {
422
+ ab?: string;
423
+ chance?: string;
424
+ type?: "string" | "boolean" | "int64" | "float" | "date" | "json";
425
+ json?: boolean;
426
+ },
427
+ ) => {
428
+ await runEnvWrite("create_only", name, value, options);
429
+ },
430
+ );
431
+
432
+ addTargetOptions(
433
+ env
434
+ .command("set")
435
+ .description("Upsert one variable")
436
+ .argument("<name>", "Variable name")
437
+ .argument("<value>", "Variable value")
438
+ .option("--ab <value-b>", "Second value for ab_roll")
439
+ .option("--chance <number>", "A-branch probability between 0 and 1")
440
+ .option("--type <type>", "Declared value type", "string")
441
+ .option("--json", "Machine-readable output", false),
442
+ ).action(
443
+ async (
444
+ name: string,
445
+ value: string,
446
+ options: EnvTargetOptions & {
447
+ ab?: string;
448
+ chance?: string;
449
+ type?: "string" | "boolean" | "int64" | "float" | "date" | "json";
450
+ json?: boolean;
451
+ },
452
+ ) => {
453
+ await runEnvWrite("upsert", name, value, options);
454
+ },
455
+ );
456
+
457
+ addTargetOptions(
458
+ env
459
+ .command("delete")
460
+ .description("Delete one variable")
461
+ .argument("<name>", "Variable name")
462
+ .option("--yes", "Skip confirmation", false)
463
+ .option("--ignore-missing", "Do not fail when variable is missing", false)
464
+ .option("--json", "Machine-readable output", false),
465
+ ).action(
466
+ async (
467
+ name: string,
468
+ options: EnvTargetOptions & { yes?: boolean; ignoreMissing?: boolean; json?: boolean },
469
+ ) => {
470
+ await runEnvDelete(name, options);
471
+ },
472
+ );
473
+
474
+ addTargetOptions(
475
+ env
476
+ .command("pull")
477
+ .description("Pull resolved variables for a project stage")
478
+ .option("--format <type>", "Output format: dotenv|json", "dotenv")
479
+ .option("--out <path>", "Output file path")
480
+ .option("--seed <value>", "Deterministic seed")
481
+ .option("--key <value>", "Deterministic key")
482
+ .option("--redact", "Print only summary in non-file mode", false),
483
+ ).action(
484
+ async (
485
+ options: EnvTargetOptions & {
486
+ format?: "dotenv" | "json";
487
+ out?: string;
488
+ seed?: string;
489
+ key?: string;
490
+ redact?: boolean;
491
+ },
492
+ ) => {
493
+ await runEnvPull(options);
494
+ },
495
+ );
496
+ }
@@ -0,0 +1,38 @@
1
+ import { BarekeyClient } from "@barekey/sdk";
2
+ import { Command } from "commander";
3
+
4
+ import {
5
+ addTargetOptions,
6
+ requireLocalSession,
7
+ resolveTarget,
8
+ type EnvTargetOptions,
9
+ } from "../command-utils.js";
10
+
11
+ async function runTypegen(options: EnvTargetOptions): Promise<void> {
12
+ const local = await requireLocalSession();
13
+ const target = await resolveTarget(options, local);
14
+ const organization = target.orgSlug ?? local.credentials.orgSlug;
15
+ if (!organization || organization.trim().length === 0) {
16
+ throw new Error("Organization slug is required.");
17
+ }
18
+
19
+ const client = new BarekeyClient({
20
+ organization,
21
+ project: target.projectSlug,
22
+ environment: target.stageSlug,
23
+ typegen: false,
24
+ });
25
+ const result = await client.typegen();
26
+
27
+ console.log(`${result.written ? "Updated" : "Unchanged"} ${result.path}`);
28
+ }
29
+
30
+ export function registerTypegenCommand(program: Command): void {
31
+ addTargetOptions(
32
+ program
33
+ .command("typegen")
34
+ .description("Generate Barekey SDK types in the installed @barekey/sdk module"),
35
+ ).action(async (options: EnvTargetOptions) => {
36
+ await runTypegen(options);
37
+ });
38
+ }
@@ -0,0 +1,4 @@
1
+ export const CLI_NAME = "barekey";
2
+ export const CLI_DESCRIPTION = "Barekey CLI";
3
+ export const CLI_VERSION = "0.2.0";
4
+ export const DEFAULT_BAREKEY_API_URL = "https://api.barekey.dev";