@configdirector/cli 0.16.0 → 0.17.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,12 @@ All notable changes to `@configdirector/cli` are documented here. The format fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.0] - 2026-11-01
8
+
9
+ ### Added
10
+
11
+ - `configdirector configs copy <key> --key <new key>` copies a config under a new key, in its project or, with `--to <project slug>`, in another project of the organization. The copy gets the config's settings and tags, and every environment's targeting rules and default targeting rule value, and starts as a new config with no old keys. In another project, environments are matched by slug, tags by name and segments by key, and `--environment <destination slug>=<source slug>`, repeatable, names the environment to copy from for an environment there with no environment of its slug in the config's project. The command prints the plan: each environment with the environment its rules come from, how many rules were copied and each rule dropped because the other project has no segment with its key, the environments it skipped, and the tags matched, created and skipped. `--dry-run` prints the plan without creating anything, and `--json` prints the Admin API's response as received. A dropped rule is not a failure: the command exits with 0, and with 2 when ConfigDirector refuses the copy, for example because the key is already in use or an environment has nothing to copy from.
12
+
7
13
  ## [0.16.0] - 2026-10-30
8
14
 
9
15
  ### Changed
package/README.md CHANGED
@@ -1,6 +1,6 @@
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. 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.
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 and copies 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
 
@@ -246,6 +246,35 @@ When the project already has a config with the key, as its current key or an old
246
246
 
247
247
  `configs create` needs `projects:read`, `configs:create`, and `configs:read` to show the config that already has the key; `--tag` also needs `tags:read`, and `--new-tag` needs `tags:create`. The config shows in the audit log with the source CLI.
248
248
 
249
+ ## Copy a config
250
+
251
+ ```bash
252
+ configdirector configs copy search-ranking --key search-ranking --to mobile-app --environment qa=staging
253
+ ```
254
+
255
+ `configs copy` copies a config under the key `--key` gives, in its project or, with `--to <project slug>`, in another project of the organization, and prints the plan:
256
+
257
+ ```txt
258
+ Copied search-ranking in web-app to search-ranking in mobile-app.
259
+ Environments:
260
+ production (live) from production 2 rules copied rule 3 dropped: mobile-app has no segment beta-testers
261
+ qa from staging 1 rule copied
262
+ Skipped source environments:
263
+ test mobile-app has no environment with this slug; create it there to copy its rules
264
+ Tags:
265
+ search matched
266
+ growth created
267
+ ```
268
+
269
+ - The copy gets the config's role, lifetime, type, constraints, variations, SDK availability, description and tags, and every environment's targeting rules and default targeting rule value. It starts as a new config, with no old keys, evaluations or code references, and shares nothing with the config it came from. An archived config cannot be copied.
270
+ - In another project, environments are matched by slug, tags by name and segments by key. An environment there with no environment of the same slug in the config's project needs `--environment <destination slug>=<source slug>`, repeatable, to name the environment it copies from; `--environment` can also replace a slug match. A source environment with no environment of its slug there is skipped.
271
+ - A targeting rule that uses a segment the other project doesn't have is dropped, and the plan names it by its position and the segment's key. A tag the other project doesn't have is created when the token has `tags:create`, and skipped otherwise.
272
+ - `--dry-run` prints the plan without creating anything, saying what would be copied, dropped, skipped and created.
273
+
274
+ The command exits with 0 when it copied the config or printed the plan, dropped rules included, and with 2 when ConfigDirector refuses the copy, for example because the key is in use or an environment has nothing to copy from, listing the reasons it gave. With `--json`, it prints the Admin API's response as received: the plan, and the created config with its targeting rules unless `--dry-run` was given.
275
+
276
+ `configs copy` needs `projects:read` and `configs:create`, and `tags:create` to create the tags another project doesn't have. The copy shows in the audit log with the source CLI.
277
+
249
278
  ## Change targeting rules
250
279
 
251
280
  ```bash
@@ -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.16.0";
15
+ var version = "0.17.0";
16
16
  //#endregion
17
17
  //#region src/CliError.ts
18
18
  var CliError = class extends Error {
@@ -96,6 +96,27 @@ var UnexpectedApiResponseError = class extends CliError {
96
96
  }
97
97
  };
98
98
  //#endregion
99
+ //#region src/admin-api/config-copy.ts
100
+ const CopyPlanSchema = v.object({
101
+ environments: v.array(v.object({
102
+ slug: v.string(),
103
+ live: v.boolean(),
104
+ sourceSlug: v.string(),
105
+ rulesCopied: v.number(),
106
+ droppedRules: v.array(v.object({
107
+ position: v.number(),
108
+ segmentKey: v.string()
109
+ }))
110
+ })),
111
+ skippedSourceEnvironments: v.array(v.string()),
112
+ tags: v.object({
113
+ matched: v.array(v.string()),
114
+ created: v.array(v.string()),
115
+ skipped: v.array(v.string())
116
+ })
117
+ });
118
+ const CopyPlanResponseSchema = v.object({ plan: CopyPlanSchema });
119
+ //#endregion
99
120
  //#region src/admin-api/config-creation.ts
100
121
  const CreatedConfigSchema = v.object({
101
122
  key: v.string(),
@@ -516,6 +537,12 @@ var AdminApiClient = class {
516
537
  body: request
517
538
  });
518
539
  }
540
+ copyConfig(projectId, configKey, request) {
541
+ return this.requestIfFound(`/v1/projects/${encodeURIComponent(projectId)}/configs/${encodeURIComponent(configKey)}/copy`, CopyPlanResponseSchema, {
542
+ method: "POST",
543
+ body: request
544
+ });
545
+ }
519
546
  async evaluateConfig(projectId, configKey, request) {
520
547
  const { responseBody, parsedBody } = await this.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/configs/${encodeURIComponent(configKey)}/evaluate`, ConfigEvaluationSchema, {
521
548
  method: "POST",
@@ -973,9 +1000,9 @@ const settingLines = (config) => [
973
1000
  `Lifecycle state: ${config.state}`,
974
1001
  ...config.tags.length === 0 ? [] : [`Tags: ${config.tags.map((tag) => tag.name).join(", ")}`]
975
1002
  ];
976
- const environmentHeading = (targetingRules) => targetingRules.environment.live ? `${targetingRules.environment.slug} (live)` : targetingRules.environment.slug;
1003
+ const environmentLabel = (environment) => environment.live ? `${environment.slug} (live)` : environment.slug;
977
1004
  const targetingRuleLines = (targetingRules, segments) => [
978
- environmentHeading(targetingRules),
1005
+ environmentLabel(targetingRules.environment),
979
1006
  ...targetingRules.rules.map((rule, index) => ` ${index + 1}. ${describeRule(rule, segments)}`),
980
1007
  ` Default targeting rule: ${targetingRules.defaultValue}`
981
1008
  ];
@@ -987,6 +1014,89 @@ const configText = (config, requestedKey, segments) => [
987
1014
  ""
988
1015
  ].join("\n");
989
1016
  //#endregion
1017
+ //#region src/plural.ts
1018
+ const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
1019
+ //#endregion
1020
+ //#region src/same-slug.ts
1021
+ const sameSlug = (slug, otherSlug) => slug.toLowerCase() === otherSlug.toLowerCase();
1022
+ //#endregion
1023
+ //#region src/configs/copy-plan-text.ts
1024
+ const PLAN_WORDING = {
1025
+ copy: {
1026
+ headline: (copying) => `Copied ${copying.sourceKey} in ${copying.sourceProject.slug} to ${copying.copyKey} in ${copying.destinationProject.slug}.`,
1027
+ rulesCopied: "copied",
1028
+ ruleDropped: "dropped",
1029
+ skippedSourceEnvironmentsHeading: "Skipped source environments:",
1030
+ tagCreated: "created",
1031
+ tagSkipped: "skipped"
1032
+ },
1033
+ "dry-run": {
1034
+ headline: (copying) => `Nothing was created: this is the plan for copying ${copying.sourceKey} in ${copying.sourceProject.slug} to ${copying.copyKey} in ${copying.destinationProject.slug}.`,
1035
+ rulesCopied: "would be copied",
1036
+ ruleDropped: "would be dropped",
1037
+ skippedSourceEnvironmentsHeading: "Source environments that would be skipped:",
1038
+ tagCreated: "would be created",
1039
+ tagSkipped: "would be skipped"
1040
+ }
1041
+ };
1042
+ const droppedRuleDescription = (wordedPlan, droppedRule) => `rule ${droppedRule.position} ${wordedPlan.wording.ruleDropped}: ${wordedPlan.destinationProjectSlug} has no segment ${droppedRule.segmentKey}`;
1043
+ const environmentColumnsOf = (wordedPlan, environment) => [
1044
+ environmentLabel(environment),
1045
+ `from ${environment.sourceSlug}`,
1046
+ `${environment.rulesCopied} ${plural(environment.rulesCopied, "rule", "rules")} ${wordedPlan.wording.rulesCopied}`,
1047
+ ...environment.droppedRules.length === 0 ? [] : [environment.droppedRules.map((droppedRule) => droppedRuleDescription(wordedPlan, droppedRule)).join("; ")]
1048
+ ];
1049
+ const skippedSourceEnvironmentReasonOf = (plan, slug) => {
1050
+ const destinationEnvironment = plan.environments.find((environment) => sameSlug(environment.slug, slug));
1051
+ return destinationEnvironment === void 0 ? { reason: "no-destination-environment" } : {
1052
+ reason: "destination-copies-another-source",
1053
+ destinationEnvironment
1054
+ };
1055
+ };
1056
+ const skippedSourceEnvironmentDescription = (destinationProjectSlug, slug, skipped) => {
1057
+ switch (skipped.reason) {
1058
+ case "no-destination-environment": return `${destinationProjectSlug} has no environment with this slug; create it there to copy its rules`;
1059
+ case "destination-copies-another-source": return `${skipped.destinationEnvironment.slug} in ${destinationProjectSlug} copies from ${skipped.destinationEnvironment.sourceSlug} instead; --environment ${skipped.destinationEnvironment.slug}=${slug} copies this one`;
1060
+ }
1061
+ };
1062
+ const skippedSourceEnvironmentRowsOf = ({ plan, destinationProjectSlug }) => plan.skippedSourceEnvironments.map((slug) => [slug, skippedSourceEnvironmentDescription(destinationProjectSlug, slug, skippedSourceEnvironmentReasonOf(plan, slug))]);
1063
+ const tagRowsOf = ({ plan, wording }) => [
1064
+ ...plan.tags.matched.map((name) => [name, "matched"]),
1065
+ ...plan.tags.created.map((name) => [name, wording.tagCreated]),
1066
+ ...plan.tags.skipped.map((name) => [name, `${wording.tagSkipped}: the API token cannot create tags`])
1067
+ ];
1068
+ const sectionLines = (heading, rows) => rows.length === 0 ? [] : [heading, ...alignedColumns(rows).map((line) => ` ${line}`)];
1069
+ const copyPlanText = (copying, plan, mode) => {
1070
+ const wordedPlan = {
1071
+ plan,
1072
+ destinationProjectSlug: copying.destinationProject.slug,
1073
+ wording: PLAN_WORDING[mode]
1074
+ };
1075
+ return [
1076
+ wordedPlan.wording.headline(copying),
1077
+ ...sectionLines("Environments:", plan.environments.map((environment) => environmentColumnsOf(wordedPlan, environment))),
1078
+ ...sectionLines(wordedPlan.wording.skippedSourceEnvironmentsHeading, skippedSourceEnvironmentRowsOf(wordedPlan)),
1079
+ ...sectionLines("Tags:", tagRowsOf(wordedPlan)),
1080
+ ""
1081
+ ].join("\n");
1082
+ };
1083
+ //#endregion
1084
+ //#region src/configs/environment-mapping.ts
1085
+ const MAPPING_SEPARATOR = "=";
1086
+ const mappedSlugsOf = (mapping) => {
1087
+ const separatorIndex = mapping.indexOf(MAPPING_SEPARATOR);
1088
+ const destinationSlug = mapping.slice(0, separatorIndex);
1089
+ const sourceSlug = mapping.slice(separatorIndex + 1);
1090
+ if (separatorIndex === -1 || destinationSlug === "" || sourceSlug === "") throw new CliError(`--environment takes <destination slug>=<source slug>, not ${mapping}.`);
1091
+ return [destinationSlug, sourceSlug];
1092
+ };
1093
+ const environmentMappingOf = (mappings) => {
1094
+ const mappedSlugs = (mappings ?? []).map(mappedSlugsOf);
1095
+ const repeatedDestinationSlug = mappedSlugs.find(([destinationSlug], index) => mappedSlugs.slice(0, index).some(([earlierDestinationSlug]) => sameSlug(earlierDestinationSlug, destinationSlug)))?.[0];
1096
+ if (repeatedDestinationSlug !== void 0) throw new CliError(`--environment maps the destination environment "${repeatedDestinationSlug}" more than once. Case doesn't matter in slugs.`);
1097
+ return Object.fromEntries(mappedSlugs);
1098
+ };
1099
+ //#endregion
990
1100
  //#region src/configs/new-config.ts
991
1101
  const CONFIG_TYPES = [
992
1102
  "boolean",
@@ -1237,9 +1347,6 @@ const newConfigOf = async (options, workingDirectory) => {
1237
1347
  };
1238
1348
  };
1239
1349
  //#endregion
1240
- //#region src/plural.ts
1241
- const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
1242
- //#endregion
1243
1350
  //#region src/project-selection.ts
1244
1351
  const requireProjectSlug = (context) => {
1245
1352
  if (context.projectSlug === void 0) throw new CliError("Name the project with --project <slug> or CONFIGDIRECTOR_PROJECT.");
@@ -1250,8 +1357,7 @@ const requireLinkedProjectSlug = (context, settings) => {
1250
1357
  if (slug === void 0) throw new CliError("Name the project with --project <slug>, CONFIGDIRECTOR_PROJECT, or `project` in configdirector.json, which `configdirector init` writes.");
1251
1358
  return slug;
1252
1359
  };
1253
- const findProjectBySlug = async (client, slug) => {
1254
- const { parsedBody: projects } = await client.listProjects();
1360
+ const projectWithSlug = (projects, slug) => {
1255
1361
  const project = projects.find((listedProject) => listedProject.slug === slug);
1256
1362
  if (project === void 0) throw new CliError(`No project in the API token's organization has the slug "${slug}".`);
1257
1363
  return project;
@@ -1279,11 +1385,13 @@ const connectToLinkedProject = async (context) => {
1279
1385
  baseUrl: context.baseUrl,
1280
1386
  token: apiToken.token
1281
1387
  });
1282
- const project = await findProjectBySlug(client, projectSlug);
1388
+ const { parsedBody: organizationProjects } = await client.listProjects();
1389
+ const project = projectWithSlug(organizationProjects, projectSlug);
1283
1390
  refuseProjectOfAnotherOrganization(project, settings, apiToken);
1284
1391
  return {
1285
1392
  client,
1286
1393
  project,
1394
+ organizationProjects,
1287
1395
  settings
1288
1396
  };
1289
1397
  };
@@ -1473,6 +1581,29 @@ const createConfig = async (context, _commandArguments, options) => {
1473
1581
  throw error;
1474
1582
  }
1475
1583
  };
1584
+ const copyConfig = async (context, [configKey], options) => {
1585
+ const environmentMapping = environmentMappingOf(options.environment);
1586
+ const mode = options.dryRun ? "dry-run" : "copy";
1587
+ const { client, project, organizationProjects } = await connectToLinkedProject(context);
1588
+ const copying = {
1589
+ sourceKey: configKey,
1590
+ sourceProject: project,
1591
+ copyKey: options.key,
1592
+ destinationProject: options.to === void 0 ? project : projectWithSlug(organizationProjects, options.to)
1593
+ };
1594
+ const receivedPlan = await client.copyConfig(project.id, configKey, {
1595
+ projectId: copying.destinationProject.id,
1596
+ key: copying.copyKey,
1597
+ environments: environmentMapping,
1598
+ dryRun: mode === "dry-run"
1599
+ });
1600
+ if (receivedPlan === void 0) throw new CliError(`No config in ${project.slug} has the key "${configKey}".`);
1601
+ return {
1602
+ json: receivedPlan.responseBody,
1603
+ text: copyPlanText(copying, receivedPlan.parsedBody.plan, mode),
1604
+ exitCode: ExitCode.success
1605
+ };
1606
+ };
1476
1607
  const archiveConfig = async (context, [configKey]) => {
1477
1608
  const { client, project } = await connectToLinkedProject(context);
1478
1609
  if (await client.archiveConfig(project.id, configKey) === "not-found") throw new CliError(`No config in ${project.slug} has the key "${configKey}".`);
@@ -5064,11 +5195,12 @@ const run = async (invocation) => {
5064
5195
  program.command("api-token").description("describe the API token a command here would use").command("get").description("show the token's organization, name, permissions and expiration, and where it came from; never the token itself").action(commandAction(getApiToken));
5065
5196
  program.command("projects").description("list the projects of the API token's organization").command("list").description("list the organization's projects, whatever project is selected").option("--no-headers", NO_HEADERS_DESCRIPTION).action(commandAction(listProjects));
5066
5197
  program.command("environments").description("list the project's environments").command("list").description("list the project's environments with their slugs, marking the live ones").option("--no-headers", NO_HEADERS_DESCRIPTION).action(commandAction(listEnvironments));
5067
- const configsCommand = program.command("configs").description("list, show, create and archive a project's configs");
5198
+ const configsCommand = program.command("configs").description("list, show, create, copy and archive a project's configs");
5068
5199
  configsCommand.command("list").description("list the project's configs with their type, role, lifetime and lifecycle state").option("--tag <name>", "list only the configs that have this tag; repeatable, for configs with any of them", repeatedOption).addOption(new Option("--untagged", "list only the configs without tags").conflicts("tag")).addOption(new Option("--state <state>", "list only the configs in this lifecycle state").choices(LIFECYCLE_STATES)).option("--no-headers", NO_HEADERS_DESCRIPTION).action(commandAction(listConfigs));
5069
5200
  configsCommand.command("get").argument("<key>", "the config's key, or an old key of it").description("show a config's settings and its targeting rules in every environment").action(commandAction(getConfig));
5070
5201
  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").option("--no-headers", NO_HEADERS_DESCRIPTION).action(commandAction(showConfigActivity));
5071
5202
  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 with --role config, while a feature flag and an experiment are always temporary and a kill switch always permanent").choices(CONFIG_LIFETIMES)).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("--new-tag <name>", "create a tag with this name in the project's tag catalog and assign it; 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));
5203
+ configsCommand.command("copy").argument("<key>", "the key of the config to copy, or an old key of it").description("copy a config under a new key, in its project or in another project of the organization, and print the plan").requiredOption("--key <key>", "the copy's key (required)").option("--to <project slug>", "slug of the project to copy the config into, another project of the organization (default: the config's project)").option("--environment <destination slug>=<source slug>", "the source environment whose targeting rules an environment of the destination project starts with, needed for each destination environment with no source environment of its slug; repeatable", repeatedOption).option("--dry-run", "print the plan without creating anything").addHelpText("after", "\nThe copy gets the config's role, lifetime, type, constraints, variations, SDK availability, description and tags, and every environment's targeting rules and default targeting rule value. It starts as a new config, with no old keys, evaluations or code references, and shares nothing with the config it came from. An archived config cannot be copied; restore it first. In another project, environments are matched by slug, tags by name and segments by key: a source environment with no environment of its slug there is skipped, and an environment there with no source environment of its slug needs --environment. A targeting rule that uses a segment the other project doesn't have is dropped. A tag it doesn't have is created when the API token can create tags, and skipped otherwise. The plan names every skipped environment, dropped rule and tag; --dry-run shows it without creating anything.").action(commandAction(copyConfig));
5072
5204
  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));
5073
5205
  const segmentsCommand = program.command("segments").description("list, show, create, update and delete a project's segments");
5074
5206
  segmentsCommand.command("list").description("list the project's segments with how many configs use each").option("--no-headers", NO_HEADERS_DESCRIPTION).action(commandAction(listSegments));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Command line tool for ConfigDirector, the managed configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",