@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 +6 -0
- package/README.md +30 -1
- package/dist/configdirector.mjs +142 -10
- package/package.json +1 -1
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
|
package/dist/configdirector.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
1003
|
+
const environmentLabel = (environment) => environment.live ? `${environment.slug} (live)` : environment.slug;
|
|
977
1004
|
const targetingRuleLines = (targetingRules, segments) => [
|
|
978
|
-
|
|
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
|
|
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
|
|
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