@barekey/cli 0.4.0 → 0.5.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.
Files changed (60) hide show
  1. package/README.md +53 -12
  2. package/bun.lock +9 -3
  3. package/dist/auth-provider.js +7 -4
  4. package/dist/command-utils.js +6 -6
  5. package/dist/commands/audit.d.ts +2 -0
  6. package/dist/commands/audit.js +47 -0
  7. package/dist/commands/auth.js +31 -9
  8. package/dist/commands/billing.d.ts +2 -0
  9. package/dist/commands/billing.js +59 -0
  10. package/dist/commands/env.js +157 -125
  11. package/dist/commands/init.d.ts +2 -0
  12. package/dist/commands/init.js +32 -0
  13. package/dist/commands/org.d.ts +2 -0
  14. package/dist/commands/org.js +85 -0
  15. package/dist/commands/project.d.ts +2 -0
  16. package/dist/commands/project.js +99 -0
  17. package/dist/commands/stage.d.ts +2 -0
  18. package/dist/commands/stage.js +125 -0
  19. package/dist/commands/target-prompts.d.ts +184 -0
  20. package/dist/commands/target-prompts.js +312 -0
  21. package/dist/commands/typegen.d.ts +2 -2
  22. package/dist/commands/typegen.js +57 -32
  23. package/dist/constants.d.ts +1 -1
  24. package/dist/constants.js +1 -1
  25. package/dist/context/session-id.d.ts +11 -0
  26. package/dist/context/session-id.js +14 -0
  27. package/dist/contracts/index.d.ts +499 -0
  28. package/dist/contracts/index.js +313 -0
  29. package/dist/credentials-store.js +70 -11
  30. package/dist/http.d.ts +34 -0
  31. package/dist/http.js +56 -2
  32. package/dist/index.js +12 -0
  33. package/dist/runtime-config.js +14 -26
  34. package/dist/typegen/core.d.ts +45 -0
  35. package/dist/typegen/core.js +219 -0
  36. package/dist/types.d.ts +5 -3
  37. package/package.json +2 -2
  38. package/src/auth-provider.ts +8 -5
  39. package/src/command-utils.ts +6 -6
  40. package/src/commands/audit.ts +63 -0
  41. package/src/commands/auth.ts +45 -39
  42. package/src/commands/billing.ts +70 -0
  43. package/src/commands/env.ts +211 -218
  44. package/src/commands/init.ts +47 -0
  45. package/src/commands/org.ts +104 -0
  46. package/src/commands/project.ts +130 -0
  47. package/src/commands/stage.ts +167 -0
  48. package/src/commands/target-prompts.ts +357 -0
  49. package/src/commands/typegen.ts +71 -45
  50. package/src/constants.ts +1 -1
  51. package/src/context/session-id.ts +14 -0
  52. package/src/contracts/index.ts +376 -0
  53. package/src/credentials-store.ts +86 -12
  54. package/src/http.ts +78 -2
  55. package/src/index.ts +12 -0
  56. package/src/runtime-config.ts +19 -32
  57. package/src/typegen/core.ts +311 -0
  58. package/src/types.ts +5 -3
  59. package/test/command-utils.test.ts +47 -0
  60. package/test/credentials-store.test.ts +40 -0
@@ -1,105 +1,136 @@
1
1
  import { writeFile } from "node:fs/promises";
2
- import { cancel, confirm, isCancel } from "@clack/prompts";
3
- import { BarekeyClient } from "@barekey/sdk/server";
4
- import pc from "picocolors";
2
+ import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
5
3
  import { createCliAuthProvider } from "../auth-provider.js";
6
4
  import { addTargetOptions, dotenvEscape, parseChance, requireLocalSession, resolveTarget, toJsonOutput, } from "../command-utils.js";
5
+ import { EnvEvaluateBatchResponseSchema, EnvEvaluateResponseSchema, EnvListResponseSchema, EnvPullResponseSchema, EnvWriteResponseSchema, } from "../contracts/index.js";
7
6
  import { postJson } from "../http.js";
8
- import { loadRuntimeConfig } from "../runtime-config.js";
9
7
  import { collectOptionValues, parseRolloutFunction, parseRolloutMilestones, parseVisibility, } from "./env-helpers.js";
10
- function createEnvClient(input) {
11
- if (input.runtimeConfig !== null) {
12
- const organization = input.organization?.trim();
13
- const isStandalone = input.runtimeConfig.config.config?.mode === "standalone";
14
- if (!organization && !isStandalone) {
15
- throw new Error("Organization slug is required.");
16
- }
17
- const jsonConfig = {
18
- ...(organization ? { organization } : {}),
19
- ...(input.project.trim().length > 0 ? { project: input.project } : {}),
20
- ...(input.environment.trim().length > 0 ? { environment: input.environment } : {}),
21
- ...(input.runtimeConfig.config.config === undefined
22
- ? {}
23
- : {
24
- config: {
25
- typegen: input.runtimeConfig.config.config.typegen,
26
- mode: input.runtimeConfig.config.config.mode,
27
- },
28
- }),
29
- };
30
- return new BarekeyClient({
31
- json: jsonConfig,
32
- });
8
+ async function resolveEnvAccess(options) {
9
+ const local = await requireLocalSession();
10
+ const target = await resolveTarget(options, local);
11
+ const authProvider = createCliAuthProvider();
12
+ const accessToken = await authProvider.getAccessToken();
13
+ return {
14
+ local,
15
+ target,
16
+ accessToken,
17
+ };
18
+ }
19
+ function renderScalar(value) {
20
+ if (typeof value === "string") {
21
+ return value;
22
+ }
23
+ return JSON.stringify(value);
24
+ }
25
+ async function promptForRequiredText(currentValue, message) {
26
+ const existing = currentValue?.trim();
27
+ if (existing && existing.length > 0) {
28
+ return existing;
33
29
  }
34
- const organization = input.organization?.trim();
35
- if (!organization) {
36
- throw new Error("Organization slug is required.");
30
+ if (!process.stdout.isTTY) {
31
+ throw new Error(`${message} is required in non-interactive mode.`);
37
32
  }
38
- return new BarekeyClient({
39
- organization,
40
- project: input.project,
41
- environment: input.environment,
33
+ const prompted = await text({
34
+ message,
35
+ validate(value) {
36
+ return value.trim().length > 0 ? undefined : "This value is required.";
37
+ },
42
38
  });
39
+ if (isCancel(prompted)) {
40
+ cancel("Command canceled.");
41
+ process.exit(0);
42
+ }
43
+ return prompted.trim();
43
44
  }
44
- async function runEnvGet(name, options) {
45
- const runtime = await loadRuntimeConfig();
46
- const local = runtime?.config.config?.mode === "standalone" ? null : await requireLocalSession();
47
- const target = await resolveTarget(options, local);
48
- const client = createEnvClient({
49
- organization: target.orgSlug ?? local?.credentials.orgSlug,
50
- project: target.projectSlug,
51
- environment: target.stageSlug,
52
- runtimeConfig: runtime,
45
+ async function maybePromptVariant(writeOptions) {
46
+ if (writeOptions.ab !== undefined || writeOptions.rollout !== undefined || !process.stdout.isTTY) {
47
+ return writeOptions;
48
+ }
49
+ const selectedKind = await select({
50
+ message: "What kind of variable is this?",
51
+ options: [
52
+ { value: "secret", label: "Secret", hint: "One stored value" },
53
+ { value: "ab_roll", label: "A/B roll", hint: "Two values plus traffic split" },
54
+ { value: "rollout", label: "Rollout", hint: "Two values plus rollout milestones" },
55
+ ],
56
+ initialValue: "secret",
53
57
  });
54
- const value = await client.get(name, {
55
- seed: options.seed,
56
- key: options.key,
58
+ if (isCancel(selectedKind)) {
59
+ cancel("Command canceled.");
60
+ process.exit(0);
61
+ }
62
+ if (selectedKind === "ab_roll") {
63
+ const valueB = await promptForRequiredText(undefined, "What's the alternate value?");
64
+ const chance = await promptForRequiredText(writeOptions.chance, "What's the A-branch probability between 0 and 1?");
65
+ return {
66
+ ...writeOptions,
67
+ ab: valueB,
68
+ chance,
69
+ };
70
+ }
71
+ if (selectedKind === "rollout") {
72
+ const valueB = await promptForRequiredText(undefined, "What's the rollout value B?");
73
+ return {
74
+ ...writeOptions,
75
+ rollout: valueB,
76
+ };
77
+ }
78
+ return writeOptions;
79
+ }
80
+ async function runEnvGet(name, options) {
81
+ const resolvedName = await promptForRequiredText(name, "What's the name of this variable?");
82
+ const { local, target, accessToken } = await resolveEnvAccess(options);
83
+ const value = await postJson({
84
+ baseUrl: local.baseUrl,
85
+ path: "/v1/env/evaluate",
86
+ accessToken,
87
+ payload: {
88
+ orgSlug: target.orgSlug,
89
+ projectSlug: target.projectSlug,
90
+ stageSlug: target.stageSlug,
91
+ name: resolvedName,
92
+ seed: options.seed,
93
+ key: options.key,
94
+ },
95
+ schema: EnvEvaluateResponseSchema,
57
96
  });
58
97
  if (options.json) {
59
- toJsonOutput(true, {
60
- name,
61
- value,
62
- });
98
+ toJsonOutput(true, value);
63
99
  return;
64
100
  }
65
- console.log(String(value));
101
+ console.log(renderScalar(value.value));
66
102
  }
67
103
  async function runEnvGetMany(options) {
68
- const runtime = await loadRuntimeConfig();
69
- const local = runtime?.config.config?.mode === "standalone" ? null : await requireLocalSession();
70
- const target = await resolveTarget(options, local);
71
- const client = createEnvClient({
72
- organization: target.orgSlug ?? local?.credentials.orgSlug,
73
- project: target.projectSlug,
74
- environment: target.stageSlug,
75
- runtimeConfig: runtime,
76
- });
77
- const names = options.names
104
+ const namesCsv = await promptForRequiredText(options.names, "Which variable names do you want, comma-separated?");
105
+ const { local, target, accessToken } = await resolveEnvAccess(options);
106
+ const names = namesCsv
78
107
  .split(",")
79
108
  .map((value) => value.trim())
80
109
  .filter((value) => value.length > 0);
81
- const resolved = await Promise.all(names.map(async (resolvedName) => ({
82
- name: resolvedName,
83
- value: await client.get(resolvedName, {
110
+ const response = await postJson({
111
+ baseUrl: local.baseUrl,
112
+ path: "/v1/env/evaluate-batch",
113
+ accessToken,
114
+ payload: {
115
+ orgSlug: target.orgSlug,
116
+ projectSlug: target.projectSlug,
117
+ stageSlug: target.stageSlug,
118
+ names,
84
119
  seed: options.seed,
85
120
  key: options.key,
86
- }),
87
- })));
121
+ },
122
+ schema: EnvEvaluateBatchResponseSchema,
123
+ });
88
124
  if (options.json) {
89
- toJsonOutput(true, resolved);
125
+ toJsonOutput(true, response.values);
90
126
  return;
91
127
  }
92
- for (const value of resolved.sort((left, right) => left.name.localeCompare(right.name))) {
93
- if (value) {
94
- console.log(`${value.name}=${String(value.value)}`);
95
- }
128
+ for (const value of [...response.values].sort((left, right) => left.name.localeCompare(right.name))) {
129
+ console.log(`${value.name}=${renderScalar(value.value)}`);
96
130
  }
97
131
  }
98
132
  async function runEnvList(options) {
99
- const local = await requireLocalSession();
100
- const target = await resolveTarget(options, local);
101
- const authProvider = createCliAuthProvider();
102
- const accessToken = await authProvider.getAccessToken();
133
+ const { local, target, accessToken } = await resolveEnvAccess(options);
103
134
  const response = await postJson({
104
135
  baseUrl: local.baseUrl,
105
136
  path: "/v1/env/list",
@@ -109,6 +140,7 @@ async function runEnvList(options) {
109
140
  projectSlug: target.projectSlug,
110
141
  stageSlug: target.stageSlug,
111
142
  },
143
+ schema: EnvListResponseSchema,
112
144
  });
113
145
  if (options.json) {
114
146
  toJsonOutput(true, response.variables);
@@ -121,56 +153,57 @@ async function runEnvList(options) {
121
153
  for (const row of response.variables) {
122
154
  const chanceSuffix = row.kind === "ab_roll" ? ` chance=${row.chance ?? 0}` : "";
123
155
  const rolloutSuffix = row.kind === "rollout"
124
- ? ` ${pc.dim(`${row.rolloutFunction ?? "linear"}(${row.rolloutMilestones?.length ?? 0} milestones)`)}`
156
+ ? ` ${row.rolloutFunction ?? "linear"}(${row.rolloutMilestones?.length ?? 0} milestones)`
125
157
  : "";
126
- console.log(`${row.name} ${pc.dim(row.visibility)} ${pc.dim(row.kind)} ${pc.dim(row.declaredType)}${chanceSuffix}${rolloutSuffix}`);
158
+ console.log(`${row.name} ${row.visibility} ${row.kind} ${row.declaredType}${chanceSuffix}${rolloutSuffix}`);
127
159
  }
128
160
  }
129
161
  async function runEnvWrite(operation, name, value, options) {
130
- const local = await requireLocalSession();
131
- const target = await resolveTarget(options, local);
132
- const authProvider = createCliAuthProvider();
133
- const accessToken = await authProvider.getAccessToken();
134
- if (options.ab !== undefined && options.rollout !== undefined) {
162
+ const resolvedName = await promptForRequiredText(name, "What's the name of this variable?");
163
+ const resolvedValue = await promptForRequiredText(value, "What's the value of this variable?");
164
+ const resolvedOptions = await maybePromptVariant(options);
165
+ const { local, target, accessToken } = await resolveEnvAccess(resolvedOptions);
166
+ if (resolvedOptions.ab !== undefined && resolvedOptions.rollout !== undefined) {
135
167
  throw new Error("Use either --ab or --rollout, not both.");
136
168
  }
137
- const hasRolloutPoints = (options.point?.length ?? 0) > 0;
138
- if (options.rollout === undefined && (options.function !== undefined || hasRolloutPoints)) {
169
+ const hasRolloutPoints = (resolvedOptions.point?.length ?? 0) > 0;
170
+ if (resolvedOptions.rollout === undefined &&
171
+ (resolvedOptions.function !== undefined || hasRolloutPoints)) {
139
172
  throw new Error("--function and --point can only be used together with --rollout.");
140
173
  }
141
- if (options.ab !== undefined && (options.function !== undefined || hasRolloutPoints)) {
174
+ if (resolvedOptions.ab !== undefined && (resolvedOptions.function !== undefined || hasRolloutPoints)) {
142
175
  throw new Error("--function and --point are only supported for --rollout, not --ab.");
143
176
  }
144
- if (options.rollout !== undefined && options.chance !== undefined) {
177
+ if (resolvedOptions.rollout !== undefined && resolvedOptions.chance !== undefined) {
145
178
  throw new Error("--chance only applies to --ab.");
146
179
  }
147
- const entry = options.rollout !== undefined
180
+ const entry = resolvedOptions.rollout !== undefined
148
181
  ? {
149
- name,
150
- visibility: parseVisibility(options.visibility),
182
+ name: resolvedName,
183
+ visibility: parseVisibility(resolvedOptions.visibility),
151
184
  kind: "rollout",
152
- declaredType: options.type ?? "string",
153
- valueA: value,
154
- valueB: options.rollout,
155
- rolloutFunction: parseRolloutFunction(options.function),
156
- rolloutMilestones: parseRolloutMilestones(options.point),
185
+ declaredType: resolvedOptions.type ?? "string",
186
+ valueA: resolvedValue,
187
+ valueB: resolvedOptions.rollout,
188
+ rolloutFunction: parseRolloutFunction(resolvedOptions.function),
189
+ rolloutMilestones: parseRolloutMilestones(resolvedOptions.point),
157
190
  }
158
- : options.ab !== undefined
191
+ : resolvedOptions.ab !== undefined
159
192
  ? {
160
- name,
161
- visibility: parseVisibility(options.visibility),
193
+ name: resolvedName,
194
+ visibility: parseVisibility(resolvedOptions.visibility),
162
195
  kind: "ab_roll",
163
- declaredType: options.type ?? "string",
164
- valueA: value,
165
- valueB: options.ab,
166
- chance: parseChance(options.chance),
196
+ declaredType: resolvedOptions.type ?? "string",
197
+ valueA: resolvedValue,
198
+ valueB: resolvedOptions.ab,
199
+ chance: parseChance(resolvedOptions.chance),
167
200
  }
168
201
  : {
169
- name,
170
- visibility: parseVisibility(options.visibility),
202
+ name: resolvedName,
203
+ visibility: parseVisibility(resolvedOptions.visibility),
171
204
  kind: "secret",
172
- declaredType: options.type ?? "string",
173
- value,
205
+ declaredType: resolvedOptions.type ?? "string",
206
+ value: resolvedValue,
174
207
  };
175
208
  const result = await postJson({
176
209
  baseUrl: local.baseUrl,
@@ -184,18 +217,17 @@ async function runEnvWrite(operation, name, value, options) {
184
217
  entries: [entry],
185
218
  deletes: [],
186
219
  },
220
+ schema: EnvWriteResponseSchema,
187
221
  });
188
- if (options.json) {
222
+ if (resolvedOptions.json) {
189
223
  toJsonOutput(true, result);
190
224
  return;
191
225
  }
192
226
  console.log(`Created: ${result.createdCount}, Updated: ${result.updatedCount}, Deleted: ${result.deletedCount}`);
193
227
  }
194
228
  async function runEnvDelete(name, options) {
195
- const local = await requireLocalSession();
196
- const target = await resolveTarget(options, local);
197
- const authProvider = createCliAuthProvider();
198
- const accessToken = await authProvider.getAccessToken();
229
+ const resolvedName = await promptForRequiredText(name, "What's the name of the variable to delete?");
230
+ const { local, target, accessToken } = await resolveEnvAccess(options);
199
231
  if (!options.ignoreMissing) {
200
232
  const listed = await postJson({
201
233
  baseUrl: local.baseUrl,
@@ -206,10 +238,11 @@ async function runEnvDelete(name, options) {
206
238
  projectSlug: target.projectSlug,
207
239
  stageSlug: target.stageSlug,
208
240
  },
241
+ schema: EnvListResponseSchema,
209
242
  });
210
- const exists = listed.variables.some((row) => row.name === name);
243
+ const exists = listed.variables.some((row) => row.name === resolvedName);
211
244
  if (!exists) {
212
- throw new Error(`Variable ${name} was not found in this stage.`);
245
+ throw new Error(`Variable ${resolvedName} was not found in this stage.`);
213
246
  }
214
247
  }
215
248
  if (!options.yes) {
@@ -217,7 +250,7 @@ async function runEnvDelete(name, options) {
217
250
  throw new Error("Deletion requires --yes in non-interactive mode.");
218
251
  }
219
252
  const confirmed = await confirm({
220
- message: `Delete variable ${name}?`,
253
+ message: `Delete variable ${resolvedName}?`,
221
254
  initialValue: false,
222
255
  });
223
256
  if (isCancel(confirmed)) {
@@ -238,8 +271,9 @@ async function runEnvDelete(name, options) {
238
271
  stageSlug: target.stageSlug,
239
272
  mode: "upsert",
240
273
  entries: [],
241
- deletes: [name],
274
+ deletes: [resolvedName],
242
275
  },
276
+ schema: EnvWriteResponseSchema,
243
277
  });
244
278
  if (options.json) {
245
279
  toJsonOutput(true, result);
@@ -248,10 +282,7 @@ async function runEnvDelete(name, options) {
248
282
  console.log(`Deleted: ${result.deletedCount}`);
249
283
  }
250
284
  async function runEnvPull(options) {
251
- const local = await requireLocalSession();
252
- const target = await resolveTarget(options, local);
253
- const authProvider = createCliAuthProvider();
254
- const accessToken = await authProvider.getAccessToken();
285
+ const { local, target, accessToken } = await resolveEnvAccess(options);
255
286
  const response = await postJson({
256
287
  baseUrl: local.baseUrl,
257
288
  path: "/v1/env/pull",
@@ -263,6 +294,7 @@ async function runEnvPull(options) {
263
294
  seed: options.seed,
264
295
  key: options.key,
265
296
  },
297
+ schema: EnvPullResponseSchema,
266
298
  });
267
299
  const format = options.format ?? "dotenv";
268
300
  const sortedKeys = Object.keys(response.byName).sort((left, right) => left.localeCompare(right));
@@ -285,7 +317,7 @@ export function registerEnvCommands(program) {
285
317
  addTargetOptions(env
286
318
  .command("get")
287
319
  .description("Evaluate one variable")
288
- .argument("<name>", "Variable name")
320
+ .argument("[name]", "Variable name")
289
321
  .option("--seed <value>", "Deterministic seed")
290
322
  .option("--key <value>", "Deterministic key")
291
323
  .option("--json", "Machine-readable output", false)).action(async (name, options) => {
@@ -294,7 +326,7 @@ export function registerEnvCommands(program) {
294
326
  addTargetOptions(env
295
327
  .command("get-many")
296
328
  .description("Evaluate a batch of variables")
297
- .requiredOption("--names <csv>", "Comma-separated variable names")
329
+ .option("--names <csv>", "Comma-separated variable names")
298
330
  .option("--seed <value>", "Deterministic seed")
299
331
  .option("--key <value>", "Deterministic key")
300
332
  .option("--json", "Machine-readable output", false)).action(async (options) => {
@@ -309,8 +341,8 @@ export function registerEnvCommands(program) {
309
341
  addTargetOptions(env
310
342
  .command("new")
311
343
  .description("Create one variable")
312
- .argument("<name>", "Variable name")
313
- .argument("<value>", "Variable value")
344
+ .argument("[name]", "Variable name")
345
+ .argument("[value]", "Variable value")
314
346
  .option("--ab <value-b>", "Second value for ab_roll")
315
347
  .option("--rollout <value-b>", "Second value for rollout")
316
348
  .option("--chance <number>", "A-branch probability between 0 and 1")
@@ -324,8 +356,8 @@ export function registerEnvCommands(program) {
324
356
  addTargetOptions(env
325
357
  .command("set")
326
358
  .description("Upsert one variable")
327
- .argument("<name>", "Variable name")
328
- .argument("<value>", "Variable value")
359
+ .argument("[name]", "Variable name")
360
+ .argument("[value]", "Variable value")
329
361
  .option("--ab <value-b>", "Second value for ab_roll")
330
362
  .option("--rollout <value-b>", "Second value for rollout")
331
363
  .option("--chance <number>", "A-branch probability between 0 and 1")
@@ -339,7 +371,7 @@ export function registerEnvCommands(program) {
339
371
  addTargetOptions(env
340
372
  .command("delete")
341
373
  .description("Delete one variable")
342
- .argument("<name>", "Variable name")
374
+ .argument("[name]", "Variable name")
343
375
  .option("--yes", "Skip confirmation", false)
344
376
  .option("--ignore-missing", "Do not fail when variable is missing", false)
345
377
  .option("--json", "Machine-readable output", false)).action(async (name, options) => {
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,32 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { promptForOrganizationSlug, promptForProjectSlug, promptForStageSlug, } from "./target-prompts.js";
4
+ async function runInit(options) {
5
+ const orgSlug = await promptForOrganizationSlug(options.org);
6
+ const projectSlug = await promptForProjectSlug(orgSlug, options.project);
7
+ const stageSlug = await promptForStageSlug(orgSlug, projectSlug, options.stage);
8
+ const configPath = path.resolve(options.path?.trim() || "barekey.json");
9
+ const contents = `${JSON.stringify({
10
+ organization: orgSlug,
11
+ project: projectSlug,
12
+ environment: stageSlug,
13
+ config: {
14
+ mode: "centralized",
15
+ typegen: "semantic",
16
+ },
17
+ }, null, 2)}\n`;
18
+ await writeFile(configPath, contents, "utf8");
19
+ console.log(`Wrote ${configPath}`);
20
+ }
21
+ export function registerInitCommand(program) {
22
+ program
23
+ .command("init")
24
+ .description("Create or update barekey.json for the current repo")
25
+ .option("--org <slug>", "Organization slug")
26
+ .option("--project <slug>", "Project slug")
27
+ .option("--stage <slug>", "Stage slug")
28
+ .option("--path <path>", "Config path", "barekey.json")
29
+ .action(async (options) => {
30
+ await runInit(options);
31
+ });
32
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerOrgCommands(program: Command): void;
@@ -0,0 +1,85 @@
1
+ import { text, isCancel, cancel } from "@clack/prompts";
2
+ import { createCliAuthProvider } from "../auth-provider.js";
3
+ import { requireLocalSession, toJsonOutput } from "../command-utils.js";
4
+ import { OrganizationsResponseSchema } from "../contracts/index.js";
5
+ import { getJson, postJson } from "../http.js";
6
+ async function promptForName(name) {
7
+ const existing = name?.trim();
8
+ if (existing && existing.length > 0) {
9
+ return existing;
10
+ }
11
+ if (!process.stdout.isTTY) {
12
+ throw new Error("Organization name is required in non-interactive mode.");
13
+ }
14
+ const prompted = await text({
15
+ message: "What’s the name of this organization?",
16
+ validate: (value) => (value.trim().length > 0 ? undefined : "Name is required."),
17
+ });
18
+ if (isCancel(prompted)) {
19
+ cancel("Command canceled.");
20
+ process.exit(0);
21
+ }
22
+ return prompted.trim();
23
+ }
24
+ async function runOrgList(options) {
25
+ const local = await requireLocalSession();
26
+ const authProvider = createCliAuthProvider();
27
+ const accessToken = await authProvider.getAccessToken();
28
+ const response = await getJson({
29
+ baseUrl: local.baseUrl,
30
+ path: "/v1/cli/orgs",
31
+ accessToken,
32
+ schema: OrganizationsResponseSchema,
33
+ });
34
+ if (options.json) {
35
+ toJsonOutput(true, response.organizations);
36
+ return;
37
+ }
38
+ if (response.organizations.length === 0) {
39
+ console.log("No organizations found.");
40
+ return;
41
+ }
42
+ for (const organization of response.organizations) {
43
+ console.log(`${organization.name} (${organization.slug}) ${organization.role}`);
44
+ }
45
+ }
46
+ async function runOrgCreate(name, options) {
47
+ const resolvedName = await promptForName(name);
48
+ const local = await requireLocalSession();
49
+ const authProvider = createCliAuthProvider();
50
+ const accessToken = await authProvider.getAccessToken();
51
+ const response = await postJson({
52
+ baseUrl: local.baseUrl,
53
+ path: "/v1/cli/orgs/create",
54
+ accessToken,
55
+ payload: {
56
+ name: resolvedName,
57
+ slug: options.slug?.trim() || null,
58
+ },
59
+ });
60
+ if (options.json) {
61
+ toJsonOutput(true, response);
62
+ return;
63
+ }
64
+ const organization = response.organization;
65
+ console.log(`Created organization ${organization.name} (${organization.slug}).`);
66
+ }
67
+ export function registerOrgCommands(program) {
68
+ const org = program.command("org").description("Organization management");
69
+ org
70
+ .command("list")
71
+ .description("List organizations available to the current user")
72
+ .option("--json", "Machine-readable output", false)
73
+ .action(async (options) => {
74
+ await runOrgList(options);
75
+ });
76
+ org
77
+ .command("create")
78
+ .description("Create an organization")
79
+ .argument("[name]", "Organization name")
80
+ .option("--slug <slug>", "Organization slug")
81
+ .option("--json", "Machine-readable output", false)
82
+ .action(async (name, options) => {
83
+ await runOrgCreate(name, options);
84
+ });
85
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerProjectCommands(program: Command): void;
@@ -0,0 +1,99 @@
1
+ import { cancel, confirm, isCancel, text } from "@clack/prompts";
2
+ import { toJsonOutput } from "../command-utils.js";
3
+ import { createProjectForOrganization, deleteProjectForOrganization, listProjectsForOrganization, promptForOrganizationSlug, promptForProjectSlug, } from "./target-prompts.js";
4
+ async function promptForProjectName(name) {
5
+ const existing = name?.trim();
6
+ if (existing && existing.length > 0) {
7
+ return existing;
8
+ }
9
+ if (!process.stdout.isTTY) {
10
+ throw new Error("Project name is required in non-interactive mode.");
11
+ }
12
+ const prompted = await text({
13
+ message: "What’s the name of this project?",
14
+ validate: (value) => (value.trim().length > 0 ? undefined : "Project name is required."),
15
+ });
16
+ if (isCancel(prompted)) {
17
+ cancel("Command canceled.");
18
+ process.exit(0);
19
+ }
20
+ return prompted.trim();
21
+ }
22
+ async function runProjectList(options) {
23
+ const orgSlug = await promptForOrganizationSlug(options.org);
24
+ const projects = await listProjectsForOrganization(orgSlug);
25
+ if (options.json) {
26
+ toJsonOutput(true, projects);
27
+ return;
28
+ }
29
+ if (projects.length === 0) {
30
+ console.log("No projects found.");
31
+ return;
32
+ }
33
+ for (const project of projects) {
34
+ console.log(`${project.name} (${project.slug}) secrets=${project.secretCount}`);
35
+ }
36
+ }
37
+ async function runProjectCreate(name, options) {
38
+ const orgSlug = await promptForOrganizationSlug(options.org);
39
+ const projectName = await promptForProjectName(name);
40
+ const project = await createProjectForOrganization(orgSlug, projectName);
41
+ if (options.json) {
42
+ toJsonOutput(true, project);
43
+ return;
44
+ }
45
+ console.log(`Created project ${project.name} (${project.slug}).`);
46
+ }
47
+ export function registerProjectCommands(program) {
48
+ const project = program.command("project").description("Project management");
49
+ project
50
+ .command("list")
51
+ .description("List projects in an organization")
52
+ .option("--org <slug>", "Organization slug")
53
+ .option("--json", "Machine-readable output", false)
54
+ .action(async (options) => {
55
+ await runProjectList(options);
56
+ });
57
+ project
58
+ .command("create")
59
+ .description("Create a project")
60
+ .argument("[name]", "Project name")
61
+ .option("--org <slug>", "Organization slug")
62
+ .option("--json", "Machine-readable output", false)
63
+ .action(async (name, options) => {
64
+ await runProjectCreate(name, options);
65
+ });
66
+ project
67
+ .command("delete")
68
+ .description("Delete a project")
69
+ .argument("[slug]", "Project slug")
70
+ .option("--org <slug>", "Organization slug")
71
+ .option("--yes", "Skip confirmation prompt", false)
72
+ .option("--json", "Machine-readable output", false)
73
+ .action(async (slug, options) => {
74
+ const orgSlug = await promptForOrganizationSlug(options.org);
75
+ const projectSlug = await promptForProjectSlug(orgSlug, slug);
76
+ if (!options.yes) {
77
+ if (!process.stdout.isTTY) {
78
+ throw new Error("Project deletion requires --yes in non-interactive mode.");
79
+ }
80
+ const confirmed = await confirm({
81
+ message: `Delete project ${projectSlug}?`,
82
+ initialValue: false,
83
+ });
84
+ if (isCancel(confirmed)) {
85
+ cancel("Command canceled.");
86
+ process.exit(0);
87
+ }
88
+ if (!confirmed) {
89
+ throw new Error("Delete canceled.");
90
+ }
91
+ }
92
+ const response = await deleteProjectForOrganization(orgSlug, projectSlug);
93
+ if (options.json) {
94
+ toJsonOutput(true, response);
95
+ return;
96
+ }
97
+ console.log(`Deleted project ${response.deletedProjectSlug}.`);
98
+ });
99
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerStageCommands(program: Command): void;