@barekey/cli 0.3.3 → 0.5.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.
Files changed (63) 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.d.ts +1 -1
  5. package/dist/command-utils.js +7 -6
  6. package/dist/commands/audit.d.ts +2 -0
  7. package/dist/commands/audit.js +47 -0
  8. package/dist/commands/auth.js +22 -7
  9. package/dist/commands/billing.d.ts +2 -0
  10. package/dist/commands/billing.js +62 -0
  11. package/dist/commands/env.js +158 -98
  12. package/dist/commands/init.d.ts +2 -0
  13. package/dist/commands/init.js +32 -0
  14. package/dist/commands/org.d.ts +2 -0
  15. package/dist/commands/org.js +85 -0
  16. package/dist/commands/project.d.ts +2 -0
  17. package/dist/commands/project.js +99 -0
  18. package/dist/commands/stage.d.ts +2 -0
  19. package/dist/commands/stage.js +125 -0
  20. package/dist/commands/target-prompts.d.ts +184 -0
  21. package/dist/commands/target-prompts.js +312 -0
  22. package/dist/commands/typegen.d.ts +2 -2
  23. package/dist/commands/typegen.js +61 -22
  24. package/dist/constants.d.ts +1 -1
  25. package/dist/constants.js +1 -1
  26. package/dist/context/session-id.d.ts +11 -0
  27. package/dist/context/session-id.js +14 -0
  28. package/dist/contracts/index.d.ts +491 -0
  29. package/dist/contracts/index.js +307 -0
  30. package/dist/credentials-store.js +70 -11
  31. package/dist/http.d.ts +34 -0
  32. package/dist/http.js +56 -2
  33. package/dist/index.js +12 -0
  34. package/dist/runtime-config.d.ts +4 -0
  35. package/dist/runtime-config.js +16 -11
  36. package/dist/typegen/core.d.ts +45 -0
  37. package/dist/typegen/core.js +219 -0
  38. package/dist/types.d.ts +5 -3
  39. package/package.json +2 -2
  40. package/src/auth-provider.ts +8 -5
  41. package/src/command-utils.ts +8 -7
  42. package/src/commands/audit.ts +63 -0
  43. package/src/commands/auth.ts +32 -37
  44. package/src/commands/billing.ts +73 -0
  45. package/src/commands/env.ts +215 -189
  46. package/src/commands/init.ts +47 -0
  47. package/src/commands/org.ts +104 -0
  48. package/src/commands/project.ts +130 -0
  49. package/src/commands/stage.ts +167 -0
  50. package/src/commands/target-prompts.ts +357 -0
  51. package/src/commands/typegen.ts +75 -29
  52. package/src/constants.ts +1 -1
  53. package/src/context/session-id.ts +14 -0
  54. package/src/contracts/index.ts +370 -0
  55. package/src/credentials-store.ts +86 -12
  56. package/src/http.ts +78 -2
  57. package/src/index.ts +12 -0
  58. package/src/runtime-config.ts +25 -14
  59. package/src/typegen/core.ts +311 -0
  60. package/src/types.ts +5 -3
  61. package/test/command-utils.test.ts +47 -0
  62. package/test/credentials-store.test.ts +40 -0
  63. package/test/runtime-config.test.ts +125 -0
@@ -1,77 +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
7
  import { collectOptionValues, parseRolloutFunction, parseRolloutMilestones, parseVisibility, } from "./env-helpers.js";
9
- function createEnvClient(input) {
10
- const organization = input.organization?.trim();
11
- if (!organization) {
12
- throw new Error("Organization slug is required.");
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;
13
29
  }
14
- return new BarekeyClient({
15
- organization,
16
- project: input.project,
17
- environment: input.environment,
30
+ if (!process.stdout.isTTY) {
31
+ throw new Error(`${message} is required in non-interactive mode.`);
32
+ }
33
+ const prompted = await text({
34
+ message,
35
+ validate(value) {
36
+ return value.trim().length > 0 ? undefined : "This value is required.";
37
+ },
18
38
  });
39
+ if (isCancel(prompted)) {
40
+ cancel("Command canceled.");
41
+ process.exit(0);
42
+ }
43
+ return prompted.trim();
19
44
  }
20
- async function runEnvGet(name, options) {
21
- const local = await requireLocalSession();
22
- const target = await resolveTarget(options, local);
23
- const client = createEnvClient({
24
- organization: target.orgSlug ?? local.credentials.orgSlug,
25
- project: target.projectSlug,
26
- environment: target.stageSlug,
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",
27
57
  });
28
- const value = await client.get(name, {
29
- seed: options.seed,
30
- 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,
31
96
  });
32
97
  if (options.json) {
33
- toJsonOutput(true, {
34
- name,
35
- value,
36
- });
98
+ toJsonOutput(true, value);
37
99
  return;
38
100
  }
39
- console.log(String(value));
101
+ console.log(renderScalar(value.value));
40
102
  }
41
103
  async function runEnvGetMany(options) {
42
- const local = await requireLocalSession();
43
- const target = await resolveTarget(options, local);
44
- const client = createEnvClient({
45
- organization: target.orgSlug ?? local.credentials.orgSlug,
46
- project: target.projectSlug,
47
- environment: target.stageSlug,
48
- });
49
- 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
50
107
  .split(",")
51
108
  .map((value) => value.trim())
52
109
  .filter((value) => value.length > 0);
53
- const resolved = await Promise.all(names.map(async (resolvedName) => ({
54
- name: resolvedName,
55
- 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,
56
119
  seed: options.seed,
57
120
  key: options.key,
58
- }),
59
- })));
121
+ },
122
+ schema: EnvEvaluateBatchResponseSchema,
123
+ });
60
124
  if (options.json) {
61
- toJsonOutput(true, resolved);
125
+ toJsonOutput(true, response.values);
62
126
  return;
63
127
  }
64
- for (const value of resolved.sort((left, right) => left.name.localeCompare(right.name))) {
65
- if (value) {
66
- console.log(`${value.name}=${String(value.value)}`);
67
- }
128
+ for (const value of [...response.values].sort((left, right) => left.name.localeCompare(right.name))) {
129
+ console.log(`${value.name}=${renderScalar(value.value)}`);
68
130
  }
69
131
  }
70
132
  async function runEnvList(options) {
71
- const local = await requireLocalSession();
72
- const target = await resolveTarget(options, local);
73
- const authProvider = createCliAuthProvider();
74
- const accessToken = await authProvider.getAccessToken();
133
+ const { local, target, accessToken } = await resolveEnvAccess(options);
75
134
  const response = await postJson({
76
135
  baseUrl: local.baseUrl,
77
136
  path: "/v1/env/list",
@@ -81,6 +140,7 @@ async function runEnvList(options) {
81
140
  projectSlug: target.projectSlug,
82
141
  stageSlug: target.stageSlug,
83
142
  },
143
+ schema: EnvListResponseSchema,
84
144
  });
85
145
  if (options.json) {
86
146
  toJsonOutput(true, response.variables);
@@ -93,56 +153,57 @@ async function runEnvList(options) {
93
153
  for (const row of response.variables) {
94
154
  const chanceSuffix = row.kind === "ab_roll" ? ` chance=${row.chance ?? 0}` : "";
95
155
  const rolloutSuffix = row.kind === "rollout"
96
- ? ` ${pc.dim(`${row.rolloutFunction ?? "linear"}(${row.rolloutMilestones?.length ?? 0} milestones)`)}`
156
+ ? ` ${row.rolloutFunction ?? "linear"}(${row.rolloutMilestones?.length ?? 0} milestones)`
97
157
  : "";
98
- 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}`);
99
159
  }
100
160
  }
101
161
  async function runEnvWrite(operation, name, value, options) {
102
- const local = await requireLocalSession();
103
- const target = await resolveTarget(options, local);
104
- const authProvider = createCliAuthProvider();
105
- const accessToken = await authProvider.getAccessToken();
106
- 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) {
107
167
  throw new Error("Use either --ab or --rollout, not both.");
108
168
  }
109
- const hasRolloutPoints = (options.point?.length ?? 0) > 0;
110
- 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)) {
111
172
  throw new Error("--function and --point can only be used together with --rollout.");
112
173
  }
113
- if (options.ab !== undefined && (options.function !== undefined || hasRolloutPoints)) {
174
+ if (resolvedOptions.ab !== undefined && (resolvedOptions.function !== undefined || hasRolloutPoints)) {
114
175
  throw new Error("--function and --point are only supported for --rollout, not --ab.");
115
176
  }
116
- if (options.rollout !== undefined && options.chance !== undefined) {
177
+ if (resolvedOptions.rollout !== undefined && resolvedOptions.chance !== undefined) {
117
178
  throw new Error("--chance only applies to --ab.");
118
179
  }
119
- const entry = options.rollout !== undefined
180
+ const entry = resolvedOptions.rollout !== undefined
120
181
  ? {
121
- name,
122
- visibility: parseVisibility(options.visibility),
182
+ name: resolvedName,
183
+ visibility: parseVisibility(resolvedOptions.visibility),
123
184
  kind: "rollout",
124
- declaredType: options.type ?? "string",
125
- valueA: value,
126
- valueB: options.rollout,
127
- rolloutFunction: parseRolloutFunction(options.function),
128
- 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),
129
190
  }
130
- : options.ab !== undefined
191
+ : resolvedOptions.ab !== undefined
131
192
  ? {
132
- name,
133
- visibility: parseVisibility(options.visibility),
193
+ name: resolvedName,
194
+ visibility: parseVisibility(resolvedOptions.visibility),
134
195
  kind: "ab_roll",
135
- declaredType: options.type ?? "string",
136
- valueA: value,
137
- valueB: options.ab,
138
- chance: parseChance(options.chance),
196
+ declaredType: resolvedOptions.type ?? "string",
197
+ valueA: resolvedValue,
198
+ valueB: resolvedOptions.ab,
199
+ chance: parseChance(resolvedOptions.chance),
139
200
  }
140
201
  : {
141
- name,
142
- visibility: parseVisibility(options.visibility),
202
+ name: resolvedName,
203
+ visibility: parseVisibility(resolvedOptions.visibility),
143
204
  kind: "secret",
144
- declaredType: options.type ?? "string",
145
- value,
205
+ declaredType: resolvedOptions.type ?? "string",
206
+ value: resolvedValue,
146
207
  };
147
208
  const result = await postJson({
148
209
  baseUrl: local.baseUrl,
@@ -156,18 +217,17 @@ async function runEnvWrite(operation, name, value, options) {
156
217
  entries: [entry],
157
218
  deletes: [],
158
219
  },
220
+ schema: EnvWriteResponseSchema,
159
221
  });
160
- if (options.json) {
222
+ if (resolvedOptions.json) {
161
223
  toJsonOutput(true, result);
162
224
  return;
163
225
  }
164
226
  console.log(`Created: ${result.createdCount}, Updated: ${result.updatedCount}, Deleted: ${result.deletedCount}`);
165
227
  }
166
228
  async function runEnvDelete(name, options) {
167
- const local = await requireLocalSession();
168
- const target = await resolveTarget(options, local);
169
- const authProvider = createCliAuthProvider();
170
- 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);
171
231
  if (!options.ignoreMissing) {
172
232
  const listed = await postJson({
173
233
  baseUrl: local.baseUrl,
@@ -178,10 +238,11 @@ async function runEnvDelete(name, options) {
178
238
  projectSlug: target.projectSlug,
179
239
  stageSlug: target.stageSlug,
180
240
  },
241
+ schema: EnvListResponseSchema,
181
242
  });
182
- const exists = listed.variables.some((row) => row.name === name);
243
+ const exists = listed.variables.some((row) => row.name === resolvedName);
183
244
  if (!exists) {
184
- throw new Error(`Variable ${name} was not found in this stage.`);
245
+ throw new Error(`Variable ${resolvedName} was not found in this stage.`);
185
246
  }
186
247
  }
187
248
  if (!options.yes) {
@@ -189,7 +250,7 @@ async function runEnvDelete(name, options) {
189
250
  throw new Error("Deletion requires --yes in non-interactive mode.");
190
251
  }
191
252
  const confirmed = await confirm({
192
- message: `Delete variable ${name}?`,
253
+ message: `Delete variable ${resolvedName}?`,
193
254
  initialValue: false,
194
255
  });
195
256
  if (isCancel(confirmed)) {
@@ -210,8 +271,9 @@ async function runEnvDelete(name, options) {
210
271
  stageSlug: target.stageSlug,
211
272
  mode: "upsert",
212
273
  entries: [],
213
- deletes: [name],
274
+ deletes: [resolvedName],
214
275
  },
276
+ schema: EnvWriteResponseSchema,
215
277
  });
216
278
  if (options.json) {
217
279
  toJsonOutput(true, result);
@@ -220,10 +282,7 @@ async function runEnvDelete(name, options) {
220
282
  console.log(`Deleted: ${result.deletedCount}`);
221
283
  }
222
284
  async function runEnvPull(options) {
223
- const local = await requireLocalSession();
224
- const target = await resolveTarget(options, local);
225
- const authProvider = createCliAuthProvider();
226
- const accessToken = await authProvider.getAccessToken();
285
+ const { local, target, accessToken } = await resolveEnvAccess(options);
227
286
  const response = await postJson({
228
287
  baseUrl: local.baseUrl,
229
288
  path: "/v1/env/pull",
@@ -235,6 +294,7 @@ async function runEnvPull(options) {
235
294
  seed: options.seed,
236
295
  key: options.key,
237
296
  },
297
+ schema: EnvPullResponseSchema,
238
298
  });
239
299
  const format = options.format ?? "dotenv";
240
300
  const sortedKeys = Object.keys(response.byName).sort((left, right) => left.localeCompare(right));
@@ -257,7 +317,7 @@ export function registerEnvCommands(program) {
257
317
  addTargetOptions(env
258
318
  .command("get")
259
319
  .description("Evaluate one variable")
260
- .argument("<name>", "Variable name")
320
+ .argument("[name]", "Variable name")
261
321
  .option("--seed <value>", "Deterministic seed")
262
322
  .option("--key <value>", "Deterministic key")
263
323
  .option("--json", "Machine-readable output", false)).action(async (name, options) => {
@@ -266,7 +326,7 @@ export function registerEnvCommands(program) {
266
326
  addTargetOptions(env
267
327
  .command("get-many")
268
328
  .description("Evaluate a batch of variables")
269
- .requiredOption("--names <csv>", "Comma-separated variable names")
329
+ .option("--names <csv>", "Comma-separated variable names")
270
330
  .option("--seed <value>", "Deterministic seed")
271
331
  .option("--key <value>", "Deterministic key")
272
332
  .option("--json", "Machine-readable output", false)).action(async (options) => {
@@ -281,8 +341,8 @@ export function registerEnvCommands(program) {
281
341
  addTargetOptions(env
282
342
  .command("new")
283
343
  .description("Create one variable")
284
- .argument("<name>", "Variable name")
285
- .argument("<value>", "Variable value")
344
+ .argument("[name]", "Variable name")
345
+ .argument("[value]", "Variable value")
286
346
  .option("--ab <value-b>", "Second value for ab_roll")
287
347
  .option("--rollout <value-b>", "Second value for rollout")
288
348
  .option("--chance <number>", "A-branch probability between 0 and 1")
@@ -296,8 +356,8 @@ export function registerEnvCommands(program) {
296
356
  addTargetOptions(env
297
357
  .command("set")
298
358
  .description("Upsert one variable")
299
- .argument("<name>", "Variable name")
300
- .argument("<value>", "Variable value")
359
+ .argument("[name]", "Variable name")
360
+ .argument("[value]", "Variable value")
301
361
  .option("--ab <value-b>", "Second value for ab_roll")
302
362
  .option("--rollout <value-b>", "Second value for rollout")
303
363
  .option("--chance <number>", "A-branch probability between 0 and 1")
@@ -311,7 +371,7 @@ export function registerEnvCommands(program) {
311
371
  addTargetOptions(env
312
372
  .command("delete")
313
373
  .description("Delete one variable")
314
- .argument("<name>", "Variable name")
374
+ .argument("[name]", "Variable name")
315
375
  .option("--yes", "Skip confirmation", false)
316
376
  .option("--ignore-missing", "Do not fail when variable is missing", false)
317
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;