@configdirector/cli 0.10.0-beta.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to `@configdirector/cli` are documented here. The format fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.12.0] - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - `configdirector segments create --segment <file>` creates a segment from a JSON file, or from stdin with `--segment -`. The file holds the segment's `key`, `name` and condition `groups`, exactly as the Admin API takes them, so you can save a segment with `segments get --json`, edit it and send it. `overrides` is optional and gives an environment condition groups of its own, keyed by the environment's ID, which `environments list --json` prints. A condition without an `id`, in the groups or in an override, gets a new one, and the IDs given are kept. The CLI checks only that the file holds a JSON object: ConfigDirector checks the segment, and when it refuses it, for example because another segment has its key, the command exits with 2 and lists its reasons. It prints `Created segment beta-testers in web-app.`, then the segment as `segments get` shows it, and `--json` prints the segment as the Admin API returned it. It needs `projects:read` and `segments:create`.
12
+ - `configdirector segments update <key> --segment <file>` replaces the key, the name and the condition groups of the segment the key names with those of the file, so a different `key` renames it, and the segment keeps its own ID whatever `id` the file holds. Without `overrides` in the file, the environment overrides stay as they are; `"overrides": {}` removes them all, and a map leaves exactly those environments overridden. It prints `Updated segment beta-testers in web-app.`, then the segment, and exits with 2 when no segment in the project has the key. It needs `projects:read`, `segments:read` and `segments:update`.
13
+ - `configdirector segments delete <key>` deletes a segment and its environment overrides with no confirmation, prints `Deleted segment beta-testers from web-app.`, and `--json` prints `{ "deletedSegmentKey": "beta-testers" }` with the key as given, as the MCP server's `delete_segment` tool does. While the targeting rules of a config use the segment, it is not deleted: the command exits with 2 and lists each config with the environments whose rules use it. It exits with 2 when no segment in the project has the key, and needs `projects:read` and `segments:delete`.
14
+ - With `environments:read`, the three commands name environments by their slug, and without it, by their ID. The changes show in the audit log with the source CLI. `segments create`, `update` and `delete` do what the MCP server's `create_segment`, `update_segment` and `delete_segment` tools do.
15
+
16
+ ## [0.11.0] - 2026-10-19
17
+
18
+ ### Changed
19
+
20
+ - The CLI is published under npm's `latest` tag, with no `-beta` in its version, so `npm install --global @configdirector/cli` and `npx @configdirector/cli` get the newest release. `@beta` stays on 0.10.0-beta.1, the last beta, so remove `@beta` from your install commands, scripts and CI to keep getting updates. Commands and output may still change before 1.0, and `--help` still ends by saying so, without the word beta.
21
+
22
+ ### Fixed
23
+
24
+ - When your project's `.npmrc` sets `min-release-age`, `npx @configdirector/cli` runs the newest release that is old enough. With `@beta`, npm refused to install anything until the newest beta was that old.
25
+
7
26
  ## [0.10.0-beta.1] - 2026-10-19
8
27
 
9
28
  ### Added
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # ConfigDirector CLI
2
2
 
3
- `configdirector` is the command line tool for [ConfigDirector](https://www.configdirector.com), the managed configuration and feature flag service. It is in beta: commands and output may change before 1.0. This release signs in to one or more organizations, links a repository to a project, lists where the repository's code reads config keys, checks those keys against the project's configs, shows the value a context gets for a config and why, reads a project's environments, configs, targeting rules and segments, creates configs, replaces a config's targeting rules in an environment, archives configs, uploads the repository's code references to the project, and lists and removes the project's scanned repositories.
3
+ `configdirector` is the command line tool for [ConfigDirector](https://www.configdirector.com), the managed configuration and feature flag service. Commands and output may change before 1.0. This release signs in to one or more organizations, links a repository to a project, lists where the repository's code reads config keys, checks those keys against the project's configs, shows the value a context gets for a config and why, reads a project's environments, configs, targeting rules and segments, creates configs, replaces a config's targeting rules in an environment, archives configs, creates, updates and deletes segments, uploads the repository's code references to the project, and lists and removes the project's scanned repositories.
4
4
 
5
5
  The [CLI page of the ConfigDirector docs](https://docs.configdirector.com/integrations/cli) covers the API token, CI setup for GitHub Actions and GitLab, and what each problem `validate` reports means for your build.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install --global @configdirector/cli@beta
10
+ npm install --global @configdirector/cli
11
11
  ```
12
12
 
13
- It needs Node.js 20 or later.
13
+ It needs Node.js 20 or later. If you installed `@configdirector/cli@beta` before 0.11.0, run this again: `@beta` stays on the last beta, 0.10.0-beta.1.
14
14
 
15
15
  ## Sign in
16
16
 
@@ -274,6 +274,59 @@ configdirector targeting-rules set new-checkout --environment production --rules
274
274
 
275
275
  `targeting-rules set` needs `projects:read`, `environments:read` and `config-targeting-rules:update`. `targeting-rules reference` needs only a valid token and no project.
276
276
 
277
+ ## Change segments
278
+
279
+ ```bash
280
+ configdirector segments create --segment beta-testers.json
281
+ configdirector segments update beta-testers --segment beta-testers.json
282
+ configdirector segments delete beta-testers
283
+ ```
284
+
285
+ `segments create` creates a segment from the file, or from stdin with `--segment -`, and `segments update <key>` replaces the segment the key names with it:
286
+
287
+ ```json
288
+ {
289
+ "key": "beta-testers",
290
+ "name": "Beta testers",
291
+ "groups": [
292
+ [
293
+ {
294
+ "kind": "attribute",
295
+ "attribute": "traits",
296
+ "trait": "/email",
297
+ "operator": "ends with any of",
298
+ "targetType": "text",
299
+ "targetValues": ["@example.com"]
300
+ }
301
+ ]
302
+ ],
303
+ "overrides": {
304
+ "7e6d5c4b-3a29-4810-8f7e-6d5c4b3a2918": [
305
+ [
306
+ {
307
+ "kind": "attribute",
308
+ "attribute": "traits",
309
+ "trait": "/email",
310
+ "operator": "ends with any of",
311
+ "targetType": "text",
312
+ "targetValues": ["@staff.example.com"]
313
+ }
314
+ ]
315
+ ]
316
+ }
317
+ }
318
+ ```
319
+
320
+ - The file holds the segment's `key`, `name` and condition `groups`, exactly as the Admin API takes them. `overrides` is optional and gives an environment condition groups of its own, keyed by the environment's ID, which `environments list --json` prints. `segments get --json` prints a segment in this shape, so you can edit it and send it back; fields the Admin API does not take, such as `usages`, are ignored.
321
+ - A condition without an `id`, in the groups or in an override, gets a new one, and the IDs given are kept.
322
+ - `update` replaces the key, the name and the groups, so a different `key` renames the segment, and the segment keeps its own ID whatever `id` the file holds. Without `overrides` in the file, the environment overrides stay as they are; `"overrides": {}` removes them all, and a map leaves exactly those environments overridden.
323
+ - The CLI checks only that the file holds a JSON object. ConfigDirector checks the segment, and when it refuses it, for example because another segment has its key, the command exits with 2 and lists its reasons.
324
+ - It asks for no confirmation. It prints `Created segment beta-testers in web-app.` or `Updated segment beta-testers in web-app.`, then the segment as `segments get` shows it. With `--json`, it prints the segment as the Admin API returned it.
325
+
326
+ `segments delete <key>` deletes a segment and its environment overrides, with no confirmation, and prints `Deleted segment beta-testers from web-app.`; `--json` prints `{ "deletedSegmentKey": "beta-testers" }` with the key as given. While the targeting rules of a config use the segment, in any environment, it is not deleted: the command exits with 2 and lists each config with the environments whose rules use it, so you can remove those segment conditions with `targeting-rules set` first. A key no segment has exits with 2 for `update` and `delete`.
327
+
328
+ `segments create` needs `projects:read` and `segments:create`, `segments update` needs `projects:read`, `segments:read` and `segments:update`, and `segments delete` needs `projects:read` and `segments:delete`. Without `environments:read`, they name environments by ID instead of by slug. The change shows in the audit log with the source CLI.
329
+
277
330
  ## Archive a config
278
331
 
279
332
  ```bash
@@ -300,7 +353,7 @@ It asks for no confirmation and does not check whether the code still reads the
300
353
  | `--project <slug>` | `CONFIGDIRECTOR_PROJECT` | Project slug. Without either, every command that reads a project uses the `project` in `configdirector.json`. |
301
354
  | `--json` | | Print JSON on stdout. Errors always go to stderr. |
302
355
 
303
- The command exits with 0 when it succeeds, 1 when `validate` found a problem or `configs create` found a config that already has the key, and 2 when it could not run: a usage mistake, a rejected token, a suspended service, an unreachable API, or something the command names that does not exist, such as a config key, an environment, a saved context, a segment key, a tag or a scanned repository. A CI job can therefore tell a wrong config key from an outage.
356
+ The command exits with 0 when it succeeds, 1 when `validate` found a problem or `configs create` found a config that already has the key, and 2 when it could not run: a usage mistake, a rejected token, a suspended service, an unreachable API, or something the command names that does not exist, such as a config key, an environment, a saved context, a segment key, a tag or a scanned repository. `segments delete` also exits with 2 when the targeting rules of a config use the segment. A CI job can therefore tell a wrong config key from an outage.
304
357
 
305
358
  ## License
306
359
 
@@ -12,7 +12,7 @@ import { promisify } from "node:util";
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { text } from "node:stream/consumers";
14
14
  //#region package.json
15
- var version = "0.10.0-beta.1";
15
+ var version = "0.12.0";
16
16
  //#endregion
17
17
  //#region src/CliError.ts
18
18
  var CliError = class extends Error {
@@ -246,11 +246,20 @@ const SegmentSchema = v.object({
246
246
  usages: v.array(v.unknown())
247
247
  });
248
248
  const SegmentDefinitionSchema = v.object({
249
+ id: v.string(),
249
250
  key: v.string(),
250
251
  name: v.string(),
251
252
  groups: v.array(ConditionGroupSchema),
252
253
  overrides: v.record(v.string(), v.array(ConditionGroupSchema))
253
254
  });
255
+ const SegmentUsageSchema = v.object({
256
+ configKey: v.string(),
257
+ environmentIds: v.array(v.string())
258
+ });
259
+ const SegmentInUseBodySchema = v.object({
260
+ code: v.literal("SEGMENT_IN_USE"),
261
+ usages: v.array(SegmentUsageSchema)
262
+ });
254
263
  //#endregion
255
264
  //#region src/admin-api/targeting-rules-reference.ts
256
265
  const TargetingRulesReferenceSchema = v.object({
@@ -426,6 +435,16 @@ const failureFor = (url, response, body) => {
426
435
  }
427
436
  };
428
437
  const isMissingResource = (received) => received.response.status === 404 && v.is(MissingResourceBodySchema, received.body);
438
+ const isSegmentInUse = (received) => received.response.status === 409 && v.is(SegmentInUseBodySchema, received.body);
439
+ const segmentDeletionOutcomeOf = (received) => {
440
+ if (isMissingResource(received)) return { outcome: "not-found" };
441
+ if (isSegmentInUse(received)) return {
442
+ outcome: "in-use",
443
+ usages: received.body.usages
444
+ };
445
+ if (!received.response.ok) throw failureFor(received.url, received.response, received.body);
446
+ return { outcome: "deleted" };
447
+ };
429
448
  const parsedResponseOf = ({ url, response, body }, schema) => {
430
449
  if (!response.ok) throw failureFor(url, response, body);
431
450
  const parsedBody = v.safeParse(schema, body);
@@ -468,6 +487,21 @@ var AdminApiClient = class {
468
487
  getSegment(projectId, segmentKey) {
469
488
  return this.getIfFound(`/v1/projects/${encodeURIComponent(projectId)}/segments/${encodeURIComponent(segmentKey)}`, SegmentDefinitionSchema);
470
489
  }
490
+ createSegment(projectId, request) {
491
+ return this.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/segments`, SegmentDefinitionSchema, {
492
+ method: "POST",
493
+ body: request
494
+ });
495
+ }
496
+ updateSegment(projectId, segmentKey, request) {
497
+ return this.requestIfFound(`/v1/projects/${encodeURIComponent(projectId)}/segments/${encodeURIComponent(segmentKey)}`, SegmentDefinitionSchema, {
498
+ method: "PUT",
499
+ body: request
500
+ });
501
+ }
502
+ async deleteSegment(projectId, segmentKey) {
503
+ return segmentDeletionOutcomeOf(await this.send(`/v1/projects/${encodeURIComponent(projectId)}/segments/${encodeURIComponent(segmentKey)}`, { method: "DELETE" }));
504
+ }
471
505
  async findTagByName(projectId, name) {
472
506
  return (await this.getIfFound(`/v1/projects/${encodeURIComponent(projectId)}/tags/by-name?${new URLSearchParams({ name })}`, TagSchema))?.parsedBody;
473
507
  }
@@ -4440,6 +4474,42 @@ const scan = async (context, [path], options) => {
4440
4474
  };
4441
4475
  };
4442
4476
  //#endregion
4477
+ //#region src/request-file.ts
4478
+ const STANDARD_INPUT_ARGUMENT = "-";
4479
+ const readRequestSource = async (argumentLabel, argument, invocation) => {
4480
+ if (argument === STANDARD_INPUT_ARGUMENT) return {
4481
+ origin: `${argumentLabel} reads stdin`,
4482
+ contents: await text(invocation.stdin)
4483
+ };
4484
+ const { file, contents } = await readOptionFile(argumentLabel, argument, invocation.workingDirectory);
4485
+ return {
4486
+ origin: `${argumentLabel} names ${file}`,
4487
+ contents
4488
+ };
4489
+ };
4490
+ const readRequestFile = async (argumentLabel, argument, invocation, expectedFormatSentence) => {
4491
+ const { origin, contents } = await readRequestSource(argumentLabel, argument, invocation);
4492
+ const request = parseJson(contents);
4493
+ if (!isJsonObject(request)) throw new CliError(`${origin}, which does not hold a JSON object. ${expectedFormatSentence}`);
4494
+ return request;
4495
+ };
4496
+ const withId = (entry) => entry.id === void 0 ? {
4497
+ id: randomUUID(),
4498
+ ...entry
4499
+ } : entry;
4500
+ //#endregion
4501
+ //#region src/segments/segment-file.ts
4502
+ const readSegmentFile = (segmentArgument, invocation) => readRequestFile("--segment", segmentArgument, invocation, "It takes {\"key\": ..., \"name\": ..., \"groups\": [...]}, like the output of segments get --json.");
4503
+ const conditionWithId = (condition) => isJsonObject(condition) ? withId(condition) : condition;
4504
+ const conditionGroupWithIds = (group) => Array.isArray(group) ? group.map(conditionWithId) : group;
4505
+ const conditionGroupsWithIds = (groups) => groups.map(conditionGroupWithIds);
4506
+ const overridesWithIds = (overrides) => Object.fromEntries(Object.entries(overrides).map(([environmentId, groups]) => [environmentId, Array.isArray(groups) ? conditionGroupsWithIds(groups) : groups]));
4507
+ const withGeneratedConditionIds = (segment) => ({
4508
+ ...segment,
4509
+ ...Array.isArray(segment.groups) ? { groups: conditionGroupsWithIds(segment.groups) } : {},
4510
+ ...isJsonObject(segment.overrides) ? { overrides: overridesWithIds(segment.overrides) } : {}
4511
+ });
4512
+ //#endregion
4443
4513
  //#region src/segments/segment-text.ts
4444
4514
  const conditionGroupLines = (heading, groups) => [
4445
4515
  "",
@@ -4470,22 +4540,72 @@ const listSegments = async (context) => {
4470
4540
  exitCode: ExitCode.success
4471
4541
  };
4472
4542
  };
4473
- const environmentSlugsFor = async (client, project, segment) => {
4474
- if (Object.keys(segment.overrides).length === 0) return /* @__PURE__ */ new Map();
4543
+ const environmentSlugsById = async (client, project) => {
4475
4544
  const environments = await ifPermitted(client.listEnvironments(project.id));
4476
4545
  return new Map((environments?.parsedBody ?? []).map((environment) => [environment.id, environment.slug]));
4477
4546
  };
4547
+ const overrideEnvironmentSlugsOf = async (client, project, segment) => Object.keys(segment.overrides).length === 0 ? /* @__PURE__ */ new Map() : environmentSlugsById(client, project);
4548
+ const noSegmentWithKey = (project, segmentKey) => new CliError(`No segment in ${project.slug} has the key "${segmentKey}".`);
4478
4549
  const getSegment = async (context, [segmentKey]) => {
4479
4550
  const { client, project } = await connectToLinkedProject(context);
4480
4551
  const receivedSegment = await client.getSegment(project.id, segmentKey);
4481
- if (receivedSegment === void 0) throw new CliError(`No segment in ${project.slug} has the key "${segmentKey}".`);
4552
+ if (receivedSegment === void 0) throw noSegmentWithKey(project, segmentKey);
4482
4553
  const { parsedBody: segment, responseBody } = receivedSegment;
4483
4554
  return {
4484
4555
  json: responseBody,
4485
- text: segmentText(segment, await environmentSlugsFor(client, project, segment)),
4556
+ text: segmentText(segment, await overrideEnvironmentSlugsOf(client, project, segment)),
4557
+ exitCode: ExitCode.success
4558
+ };
4559
+ };
4560
+ const createSegment = async (context, _commandArguments, options) => {
4561
+ const requestedSegment = withGeneratedConditionIds(await readSegmentFile(options.segment, context.invocation));
4562
+ const { client, project } = await connectToLinkedProject(context);
4563
+ const { parsedBody: segment, responseBody } = await client.createSegment(project.id, requestedSegment);
4564
+ return {
4565
+ json: responseBody,
4566
+ text: [`Created segment ${segment.key} in ${project.slug}.`, segmentText(segment, await overrideEnvironmentSlugsOf(client, project, segment))].join("\n"),
4486
4567
  exitCode: ExitCode.success
4487
4568
  };
4488
4569
  };
4570
+ const updateSegment = async (context, [segmentKey], options) => {
4571
+ const requestedSegment = withGeneratedConditionIds(await readSegmentFile(options.segment, context.invocation));
4572
+ const { client, project } = await connectToLinkedProject(context);
4573
+ const currentSegment = await client.getSegment(project.id, segmentKey);
4574
+ if (currentSegment === void 0) throw noSegmentWithKey(project, segmentKey);
4575
+ const updatedSegment = await client.updateSegment(project.id, segmentKey, {
4576
+ ...requestedSegment,
4577
+ id: currentSegment.parsedBody.id
4578
+ });
4579
+ if (updatedSegment === void 0) throw noSegmentWithKey(project, segmentKey);
4580
+ const { parsedBody: segment, responseBody } = updatedSegment;
4581
+ return {
4582
+ json: responseBody,
4583
+ text: [`Updated segment ${segmentKey} in ${project.slug}.`, segmentText(segment, await overrideEnvironmentSlugsOf(client, project, segment))].join("\n"),
4584
+ exitCode: ExitCode.success
4585
+ };
4586
+ };
4587
+ const usageLine = (usage, environmentSlugs) => {
4588
+ const environmentNames = usage.environmentIds.map((environmentId) => environmentSlugs.get(environmentId) ?? environmentId);
4589
+ return ` ${usage.configKey} in ${environmentNames.join(", ")}`;
4590
+ };
4591
+ const segmentInUseError = (segmentKey, usages, environmentSlugs) => new CliError([
4592
+ `The targeting rules of ${usages.length} ${plural(usages.length, "config use", "configs use")} the segment ${segmentKey}, so it was not deleted:`,
4593
+ ...usages.map((usage) => usageLine(usage, environmentSlugs)),
4594
+ "Remove those segment conditions first, with configdirector targeting-rules set."
4595
+ ].join("\n"));
4596
+ const deleteSegment = async (context, [segmentKey]) => {
4597
+ const { client, project } = await connectToLinkedProject(context);
4598
+ const deletion = await client.deleteSegment(project.id, segmentKey);
4599
+ switch (deletion.outcome) {
4600
+ case "deleted": return {
4601
+ json: { deletedSegmentKey: segmentKey },
4602
+ text: [`Deleted segment ${segmentKey} from ${project.slug}.`, ""].join("\n"),
4603
+ exitCode: ExitCode.success
4604
+ };
4605
+ case "not-found": throw noSegmentWithKey(project, segmentKey);
4606
+ case "in-use": throw segmentInUseError(segmentKey, deletion.usages, await environmentSlugsById(client, project));
4607
+ }
4608
+ };
4489
4609
  //#endregion
4490
4610
  //#region src/targeting-rules/reference-text.ts
4491
4611
  const headedListLines = (heading, items) => [heading, ...items.map((item) => ` ${item}`)];
@@ -4505,28 +4625,7 @@ const targetingRulesReferenceText = (reference) => [
4505
4625
  ].join("\n");
4506
4626
  //#endregion
4507
4627
  //#region src/targeting-rules/targeting-rules-file.ts
4508
- const STANDARD_INPUT_ARGUMENT = "-";
4509
- const readRulesSource = async (rulesArgument, invocation) => {
4510
- if (rulesArgument === STANDARD_INPUT_ARGUMENT) return {
4511
- origin: "--rules reads stdin",
4512
- contents: await text(invocation.stdin)
4513
- };
4514
- const { file, contents } = await readOptionFile("--rules", rulesArgument, invocation.workingDirectory);
4515
- return {
4516
- origin: `--rules names ${file}`,
4517
- contents
4518
- };
4519
- };
4520
- const readTargetingRulesFile = async (rulesArgument, invocation) => {
4521
- const { origin, contents } = await readRulesSource(rulesArgument, invocation);
4522
- const targetingRules = parseJson(contents);
4523
- if (!isJsonObject(targetingRules)) throw new CliError(`${origin}, which does not hold a JSON object. It takes {"defaultValue": ..., "rules": [...]}, like an entry of targets in configs get --json.`);
4524
- return targetingRules;
4525
- };
4526
- const withId = (entry) => entry.id === void 0 ? {
4527
- id: randomUUID(),
4528
- ...entry
4529
- } : entry;
4628
+ const readTargetingRulesFile = (rulesArgument, invocation) => readRequestFile("--rules", rulesArgument, invocation, "It takes {\"defaultValue\": ..., \"rules\": [...]}, like an entry of targets in configs get --json.");
4530
4629
  const conditionOrPercentageWithId = (conditionOrPercentage) => isJsonObject(conditionOrPercentage) ? withId(conditionOrPercentage) : conditionOrPercentage;
4531
4630
  const conditionsOrPercentagesWithIds = (rule, field) => {
4532
4631
  const conditionsOrPercentages = rule[field];
@@ -4784,6 +4883,7 @@ const validate = async (context, [path]) => {
4784
4883
  //#endregion
4785
4884
  //#region src/run.ts
4786
4885
  const DEFAULT_BASE_URL = "https://api.configdirector.com";
4886
+ const SEGMENT_FILE_DESCRIPTION = "a JSON file holding the segment as the Admin API takes it, or - to read it from stdin (required)";
4787
4887
  const withoutTrailingSlashes = (url) => {
4788
4888
  let trimmedUrl = url;
4789
4889
  while (trimmedUrl.endsWith("/")) trimmedUrl = trimmedUrl.slice(0, -1);
@@ -4817,7 +4917,7 @@ const run = async (invocation) => {
4817
4917
  const commandAction = (command) => async (...actionArguments) => {
4818
4918
  exitCode = await runCommand(invocation, command, actionArguments);
4819
4919
  };
4820
- const program = new Command("configdirector").description("Work with ConfigDirector configs from the terminal and CI.").addHelpText("afterAll", "\nBeta: commands and output may change before 1.0.").configureHelp({ showGlobalOptions: true }).version(version).option("--token <token>", "API token, used instead of CONFIGDIRECTOR_TOKEN and the CLI sign-ins saved by login").option("--base-url <url>", `Admin API host, used instead of CONFIGDIRECTOR_BASE_URL (default ${DEFAULT_BASE_URL})`).option("--project <slug>", "project slug, used instead of CONFIGDIRECTOR_PROJECT").option("--organization <name or id>", "organization whose CLI sign-in to use, by its saved name or its id, used instead of CONFIGDIRECTOR_ORGANIZATION and `organization` in configdirector.json").option("--json", "print JSON on stdout").exitOverride().configureOutput({
4920
+ const program = new Command("configdirector").description("Work with ConfigDirector configs from the terminal and CI.").addHelpText("afterAll", "\nCommands and output may change before 1.0.").configureHelp({ showGlobalOptions: true }).version(version).option("--token <token>", "API token, used instead of CONFIGDIRECTOR_TOKEN and the CLI sign-ins saved by login").option("--base-url <url>", `Admin API host, used instead of CONFIGDIRECTOR_BASE_URL (default ${DEFAULT_BASE_URL})`).option("--project <slug>", "project slug, used instead of CONFIGDIRECTOR_PROJECT").option("--organization <name or id>", "organization whose CLI sign-in to use, by its saved name or its id, used instead of CONFIGDIRECTOR_ORGANIZATION and `organization` in configdirector.json").option("--json", "print JSON on stdout").exitOverride().configureOutput({
4821
4921
  writeOut: (text) => invocation.stdout.write(text),
4822
4922
  writeErr: (text) => invocation.stderr.write(text)
4823
4923
  });
@@ -4836,9 +4936,12 @@ const run = async (invocation) => {
4836
4936
  configsCommand.command("activity").argument("<key>", "the config's key, or an old key of it").description("show when SDKs last evaluated a config, per environment, SDK key kind, SDK name and SDK version").action(commandAction(showConfigActivity));
4837
4937
  configsCommand.command("create").description("create a config from options that mirror the Admin API's fields").requiredOption("--key <key>", "the config's key (required)").option("--description <text>", "a description shown with the config in the dashboard").addOption(new Option("--type <type>", "the type of the config's values (required)").choices(CONFIG_TYPES).makeOptionMandatory()).addOption(new Option("--role <role>", "what the config is for (required)").choices(CONFIG_ROLES).makeOptionMandatory()).addOption(new Option("--lifetime <lifetime>", "whether the config is expected to be removed (required)").choices(CONFIG_LIFETIMES).makeOptionMandatory()).requiredOption("--default-value <value>", "the default targeting rule value every environment starts with, in the type given by --type; not the in-code default value an application passes to the SDK (required)").option("--client-only", "serve the config to client SDKs only (default: client and server SDKs)").addOption(new Option("--server-only", "serve the config to server SDKs only (default: client and server SDKs)").conflicts("clientOnly")).option("--tag <name>", "assign the project's tag with this name; repeatable", repeatedOption).option("--variation <name=value>", "a variation of an experiment, its value in the type given by --type; repeatable", repeatedOption).addOption(new Option("--min <number>", "the smallest value allowed, included").helpGroup(typeConstraintHelpGroup("min"))).addOption(new Option("--max <number>", "the largest value allowed, included").helpGroup(typeConstraintHelpGroup("max"))).addOption(new Option("--exclusive-min <number>", "values must be greater than this number").conflicts("min").helpGroup(typeConstraintHelpGroup("exclusiveMin"))).addOption(new Option("--exclusive-max <number>", "values must be less than this number").conflicts("max").helpGroup(typeConstraintHelpGroup("exclusiveMax"))).addOption(new Option("--unit <unit>", "the unit of the number the SDKs receive (required)").choices(TIMESPAN_UNITS).helpGroup(typeConstraintHelpGroup("unit"))).addOption(new Option("--allow-fractions", "allow values with a fraction of the unit").helpGroup(typeConstraintHelpGroup("allowFractions"))).addOption(new Option("--allowed-value <value>", "a value the config can take; repeatable").argParser(repeatedOption).helpGroup(typeConstraintHelpGroup("allowedValue"))).addOption(new Option("--enum-value-type <type>", "whether the allowed values are strings or numbers (default: string)").choices(ENUM_VALUE_TYPES).helpGroup(typeConstraintHelpGroup("enumValueType"))).addOption(new Option("--https-only", "allow only HTTPS URLs").helpGroup(typeConstraintHelpGroup("httpsOnly"))).addOption(new Option("--json-schema <file>", "a file holding the JSON Schema every value must match").helpGroup(typeConstraintHelpGroup("jsonSchema"))).action(commandAction(createConfig));
4838
4938
  configsCommand.command("archive").argument("<key>", "the config's key, or an old key of it").description("archive a config, so SDKs stop receiving it; the dashboard can restore it for 30 days").addHelpText("after", "\nThe config is archived with no confirmation and no check of whether the code still reads its key, so run configdirector scan first. Applications that still read it get their in-code default value.").action(commandAction(archiveConfig));
4839
- const segmentsCommand = program.command("segments").description("list a project's segments and show one segment");
4939
+ const segmentsCommand = program.command("segments").description("list, show, create, update and delete a project's segments");
4840
4940
  segmentsCommand.command("list").description("list the project's segments with how many configs use each").action(commandAction(listSegments));
4841
4941
  segmentsCommand.command("get").argument("<key>", "the segment's key").description("show a segment's condition groups and its environment overrides").action(commandAction(getSegment));
4942
+ segmentsCommand.command("create").description("create a segment from a JSON file").requiredOption("--segment <file>", SEGMENT_FILE_DESCRIPTION).addHelpText("after", "\nThe file holds {\"key\": ..., \"name\": ..., \"groups\": [...], \"overrides\": {...}}, like the output of segments get --json, so you can edit that output and send it. groups lists the condition groups, and a context is in the segment when every condition of any one group matches. overrides is optional: it maps an environment's id, which environments list --json shows, to the condition groups used there instead. A condition without an id gets a new one.").action(commandAction(createSegment));
4943
+ segmentsCommand.command("update").argument("<key>", "the segment's key").description("replace a segment's key, name and condition groups with a JSON file, and its environment overrides when the file has them").requiredOption("--segment <file>", SEGMENT_FILE_DESCRIPTION).addHelpText("after", "\nThe file takes the same form as for segments create, and the segment keeps its id whatever id the file holds. Its key, name and condition groups are replaced, so a different key renames it. Without overrides in the file, the environment overrides stay as they are; with \"overrides\": {} they are all removed, and a map leaves exactly those environments overridden. A condition without an id gets a new one.").action(commandAction(updateSegment));
4944
+ segmentsCommand.command("delete").argument("<key>", "the segment's key").description("delete a segment and its environment overrides").addHelpText("after", "\nThe segment is deleted with no confirmation. While the targeting rules of a config use it, the deletion is refused and the configs are listed with their environments.").action(commandAction(deleteSegment));
4842
4945
  const targetingRulesCommand = program.command("targeting-rules").description("replace a config's targeting rules in one environment and show the format they take");
4843
4946
  targetingRulesCommand.command("set").argument("<key>", "the config's key, or an old key of it").description("replace a config's targeting rules in one environment with the rules of a JSON file").requiredOption("--environment <slug>", "slug of the environment whose targeting rules to replace (required)").requiredOption("--rules <file>", "a JSON file holding defaultValue and rules as the Admin API takes them, or - to read it from stdin (required)").addHelpText("after", "\nThe file holds {\"defaultValue\": ..., \"rules\": [...]}, like an entry of targets in configs get --json, so you can edit that output and write it back. Every rule of the environment is replaced. A rule, condition or percentage without an id gets a new one. configdirector targeting-rules reference lists the condition kinds, attributes and operators.").action(commandAction(setTargetingRules));
4844
4947
  targetingRulesCommand.command("reference").description("show the format targeting rules take: the condition kinds, the attributes a condition can test and the operators").action(commandAction(showTargetingRulesReference));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/cli",
3
- "version": "0.10.0-beta.1",
3
+ "version": "0.12.0",
4
4
  "description": "Command line tool for ConfigDirector, the managed configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",
@@ -37,7 +37,7 @@
37
37
  "build": "tsdown",
38
38
  "pack:tarball": "yarn pack --out package.tgz",
39
39
  "ci:test:packed-tarball": "vitest run --config vitest.packed-tarball.config.ts",
40
- "release": "yarn typecheck && yarn lint && yarn ci:test && yarn build && yarn pack:tarball && yarn ci:test:packed-tarball && npm publish package.tgz --access public --tag beta"
40
+ "release": "yarn typecheck && yarn lint && yarn ci:test && yarn build && yarn pack:tarball && yarn ci:test:packed-tarball && npm publish package.tgz --access public"
41
41
  },
42
42
  "dependencies": {
43
43
  "commander": "^14.0.3",