@configdirector/cli 0.11.0 → 0.13.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 +20 -0
- package/README.md +66 -4
- package/dist/configdirector.mjs +215 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ All notable changes to `@configdirector/cli` are documented here. The format fol
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.13.0] - 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `configdirector configs create --new-tag <name>`, repeatable, creates a tag with that name in the project's tag catalog and assigns it to the new config, so a token with `configs:create` and `tags:create` can create a config with tags the project does not have yet. `--tag <name>` still only finds tags the project already has, so a misspelled `--tag` never creates a tag, and the two can be given together. Every `--tag` name is looked up before any tag is created. A `--new-tag` name given twice, a name given with both options, or more than 20 tags in all exits with 2 before anything is sent. A `--new-tag` name a tag already has exits with 2, creates no config, and says to use `--tag` for it. When a later step fails, for example because the key is taken, the command names the tags it created, which stay in the project's tag catalog, so you can pass them with `--tag` next time.
|
|
12
|
+
- `brew install ConfigDirector/tap/configdirector` installs the CLI with Homebrew, which takes care of Node.js. The Homebrew formula is updated with every npm release, so `brew upgrade configdirector` gets the newest one.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `configdirector scan` finds configs read in Nuxt server code through `useConfigDirectorServerClient()`, the name `@configdirector/nuxt-sdk` uses from 1.11.0. Server code that calls `useConfigDirectorClient()`, as it did with 1.10.0 and earlier, is still found.
|
|
17
|
+
|
|
18
|
+
## [0.12.0] - 2026-10-19
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `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`.
|
|
23
|
+
- `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`.
|
|
24
|
+
- `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`.
|
|
25
|
+
- 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.
|
|
26
|
+
|
|
7
27
|
## [0.11.0] - 2026-10-19
|
|
8
28
|
|
|
9
29
|
### 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, 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
|
|
|
@@ -12,6 +12,14 @@ npm install --global @configdirector/cli
|
|
|
12
12
|
|
|
13
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
|
+
Or install it with Homebrew, which takes care of Node.js:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
brew install ConfigDirector/tap/configdirector
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The Homebrew formula is updated with every npm release, so `brew upgrade configdirector` gets the newest one.
|
|
22
|
+
|
|
15
23
|
## Sign in
|
|
16
24
|
|
|
17
25
|
Create an API token in the ConfigDirector dashboard, then run:
|
|
@@ -220,7 +228,8 @@ Default targeting rule in every environment: false
|
|
|
220
228
|
```
|
|
221
229
|
|
|
222
230
|
- `--key`, `--type` (`boolean`, `string`, `integer`, `float`, `timespan`, `enum`, `url` or `json`), `--role` (`config`, `flag`, `kill-switch` or `experiment`), `--lifetime` (`temporary` or `permanent`) and `--default-value` are required. `--default-value` is the default targeting rule value every environment starts with, not the in-code default value an application passes to the SDK.
|
|
223
|
-
- `--description <text>`; `--client-only` or `--server-only`, without which client and server SDKs both receive the config;
|
|
231
|
+
- `--description <text>`; `--client-only` or `--server-only`, without which client and server SDKs both receive the config; and `--variation <name=value>`, repeatable, for an experiment's variations.
|
|
232
|
+
- `--tag <name>`, repeatable, names a tag the project already has, and `--new-tag <name>`, repeatable, creates a tag with that name in the project's tag catalog. The config gets the tags of both, at most 20 in all. Every `--tag` name is looked up before any tag is created, so a misspelled `--tag` creates nothing; a name no tag has, a `--new-tag` name a tag already has, a `--new-tag` name given twice and a name given with both options exit with 2. When a later step fails, such as a key that is taken, the tags already created stay in the catalog, and the command names them so you can pass them with `--tag` next time.
|
|
224
233
|
- The values of `--default-value` and `--variation` are read in the config's type: `true` or `false`, a number for an integer, float or timespan, the text as written for a string, enum or URL, and JSON for a JSON config, with `@file` reading it from a file. A value that does not fit the type is a usage error.
|
|
225
234
|
- Constraints, each accepted only by its types: `--min` and `--max` (a value may equal them) for an integer, float or timespan; `--exclusive-min` and `--exclusive-max` for an integer or float; `--unit` (`milliseconds`, `seconds` or `minutes`) and `--allow-fractions` for a timespan, which needs `--unit`, `--min` and `--max`; `--allowed-value`, repeatable, and `--enum-value-type string|number` (default `string`) for an enum; `--https-only` for a URL, without which any HTTP or HTTPS URL is accepted; and `--json-schema <file>` for a JSON config. An option of another type is a usage error naming the types it belongs to. A timespan without `--unit`, `--min` or `--max` is a usage error too.
|
|
226
235
|
|
|
@@ -231,7 +240,7 @@ configdirector configs create --key checkout-timeout --type timespan --role conf
|
|
|
231
240
|
|
|
232
241
|
When the project already has a config with the key, as its current key or an old one, nothing is created: the command prints that config's key, type, role, lifetime and lifecycle state, and exits with 1. When ConfigDirector refuses the config for another reason, it exits with 2 and lists the reasons. With `--json`, it prints the created config as received, or the existing config when the key is taken.
|
|
233
242
|
|
|
234
|
-
`configs create` needs `projects:read`, `configs:create`, and `configs:read` to show the config that already has the key; `--tag` also needs `tags:read
|
|
243
|
+
`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.
|
|
235
244
|
|
|
236
245
|
## Change targeting rules
|
|
237
246
|
|
|
@@ -274,6 +283,59 @@ configdirector targeting-rules set new-checkout --environment production --rules
|
|
|
274
283
|
|
|
275
284
|
`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
285
|
|
|
286
|
+
## Change segments
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
configdirector segments create --segment beta-testers.json
|
|
290
|
+
configdirector segments update beta-testers --segment beta-testers.json
|
|
291
|
+
configdirector segments delete beta-testers
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`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:
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"key": "beta-testers",
|
|
299
|
+
"name": "Beta testers",
|
|
300
|
+
"groups": [
|
|
301
|
+
[
|
|
302
|
+
{
|
|
303
|
+
"kind": "attribute",
|
|
304
|
+
"attribute": "traits",
|
|
305
|
+
"trait": "/email",
|
|
306
|
+
"operator": "ends with any of",
|
|
307
|
+
"targetType": "text",
|
|
308
|
+
"targetValues": ["@example.com"]
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
],
|
|
312
|
+
"overrides": {
|
|
313
|
+
"7e6d5c4b-3a29-4810-8f7e-6d5c4b3a2918": [
|
|
314
|
+
[
|
|
315
|
+
{
|
|
316
|
+
"kind": "attribute",
|
|
317
|
+
"attribute": "traits",
|
|
318
|
+
"trait": "/email",
|
|
319
|
+
"operator": "ends with any of",
|
|
320
|
+
"targetType": "text",
|
|
321
|
+
"targetValues": ["@staff.example.com"]
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- 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.
|
|
330
|
+
- A condition without an `id`, in the groups or in an override, gets a new one, and the IDs given are kept.
|
|
331
|
+
- `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.
|
|
332
|
+
- 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.
|
|
333
|
+
- 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.
|
|
334
|
+
|
|
335
|
+
`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`.
|
|
336
|
+
|
|
337
|
+
`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.
|
|
338
|
+
|
|
277
339
|
## Archive a config
|
|
278
340
|
|
|
279
341
|
```bash
|
|
@@ -300,7 +362,7 @@ It asks for no confirmation and does not check whether the code still reads the
|
|
|
300
362
|
| `--project <slug>` | `CONFIGDIRECTOR_PROJECT` | Project slug. Without either, every command that reads a project uses the `project` in `configdirector.json`. |
|
|
301
363
|
| `--json` | | Print JSON on stdout. Errors always go to stderr. |
|
|
302
364
|
|
|
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.
|
|
365
|
+
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
366
|
|
|
305
367
|
## License
|
|
306
368
|
|
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.13.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({
|
|
@@ -386,7 +395,10 @@ const ConfigActivitySchema = v.object({ latestEvaluations: v.array(v.object({
|
|
|
386
395
|
sdkVersion: v.nullable(v.string()),
|
|
387
396
|
time: v.pipe(v.string(), v.isoTimestamp())
|
|
388
397
|
})) });
|
|
389
|
-
const TagSchema = v.object({
|
|
398
|
+
const TagSchema = v.object({
|
|
399
|
+
id: v.string(),
|
|
400
|
+
name: v.string()
|
|
401
|
+
});
|
|
390
402
|
const tagFilterQuery = (tagFilter) => {
|
|
391
403
|
switch (tagFilter.form) {
|
|
392
404
|
case "every-config": return "";
|
|
@@ -426,6 +438,16 @@ const failureFor = (url, response, body) => {
|
|
|
426
438
|
}
|
|
427
439
|
};
|
|
428
440
|
const isMissingResource = (received) => received.response.status === 404 && v.is(MissingResourceBodySchema, received.body);
|
|
441
|
+
const isSegmentInUse = (received) => received.response.status === 409 && v.is(SegmentInUseBodySchema, received.body);
|
|
442
|
+
const segmentDeletionOutcomeOf = (received) => {
|
|
443
|
+
if (isMissingResource(received)) return { outcome: "not-found" };
|
|
444
|
+
if (isSegmentInUse(received)) return {
|
|
445
|
+
outcome: "in-use",
|
|
446
|
+
usages: received.body.usages
|
|
447
|
+
};
|
|
448
|
+
if (!received.response.ok) throw failureFor(received.url, received.response, received.body);
|
|
449
|
+
return { outcome: "deleted" };
|
|
450
|
+
};
|
|
429
451
|
const parsedResponseOf = ({ url, response, body }, schema) => {
|
|
430
452
|
if (!response.ok) throw failureFor(url, response, body);
|
|
431
453
|
const parsedBody = v.safeParse(schema, body);
|
|
@@ -468,9 +490,30 @@ var AdminApiClient = class {
|
|
|
468
490
|
getSegment(projectId, segmentKey) {
|
|
469
491
|
return this.getIfFound(`/v1/projects/${encodeURIComponent(projectId)}/segments/${encodeURIComponent(segmentKey)}`, SegmentDefinitionSchema);
|
|
470
492
|
}
|
|
493
|
+
createSegment(projectId, request) {
|
|
494
|
+
return this.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/segments`, SegmentDefinitionSchema, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
body: request
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
updateSegment(projectId, segmentKey, request) {
|
|
500
|
+
return this.requestIfFound(`/v1/projects/${encodeURIComponent(projectId)}/segments/${encodeURIComponent(segmentKey)}`, SegmentDefinitionSchema, {
|
|
501
|
+
method: "PUT",
|
|
502
|
+
body: request
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async deleteSegment(projectId, segmentKey) {
|
|
506
|
+
return segmentDeletionOutcomeOf(await this.send(`/v1/projects/${encodeURIComponent(projectId)}/segments/${encodeURIComponent(segmentKey)}`, { method: "DELETE" }));
|
|
507
|
+
}
|
|
471
508
|
async findTagByName(projectId, name) {
|
|
472
509
|
return (await this.getIfFound(`/v1/projects/${encodeURIComponent(projectId)}/tags/by-name?${new URLSearchParams({ name })}`, TagSchema))?.parsedBody;
|
|
473
510
|
}
|
|
511
|
+
async createTag(projectId, name) {
|
|
512
|
+
return (await this.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/tags`, TagSchema, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body: { name }
|
|
515
|
+
})).parsedBody;
|
|
516
|
+
}
|
|
474
517
|
createConfig(projectId, request) {
|
|
475
518
|
return this.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/configs`, CreatedConfigSchema, {
|
|
476
519
|
method: "POST",
|
|
@@ -1168,6 +1211,9 @@ const newConfigOf = async (options, workingDirectory) => {
|
|
|
1168
1211
|
};
|
|
1169
1212
|
};
|
|
1170
1213
|
//#endregion
|
|
1214
|
+
//#region src/plural.ts
|
|
1215
|
+
const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
|
|
1216
|
+
//#endregion
|
|
1171
1217
|
//#region src/project-selection.ts
|
|
1172
1218
|
const requireProjectSlug = (context) => {
|
|
1173
1219
|
if (context.projectSlug === void 0) throw new CliError("Name the project with --project <slug> or CONFIGDIRECTOR_PROJECT.");
|
|
@@ -1321,13 +1367,78 @@ const creationOutput = (project, newConfig, creation) => {
|
|
|
1321
1367
|
};
|
|
1322
1368
|
}
|
|
1323
1369
|
};
|
|
1370
|
+
const MAXIMUM_CONFIG_TAGS = 20;
|
|
1371
|
+
const sameTagName = (name, otherName) => name.toLowerCase() === otherName.toLowerCase();
|
|
1372
|
+
const configTagNamesOf = (options) => {
|
|
1373
|
+
const existingTagNames = options.tag ?? [];
|
|
1374
|
+
const newTagNames = options.newTag ?? [];
|
|
1375
|
+
const repeatedNewTagName = newTagNames.find((name, index) => newTagNames.slice(0, index).some((earlierName) => sameTagName(earlierName, name)));
|
|
1376
|
+
if (repeatedNewTagName !== void 0) throw new CliError(`--new-tag gives the tag name "${repeatedNewTagName}" more than once. Case doesn't matter in tag names.`);
|
|
1377
|
+
const nameInBothOptions = newTagNames.find((name) => existingTagNames.some((existingTagName) => sameTagName(existingTagName, name)));
|
|
1378
|
+
if (nameInBothOptions !== void 0) throw new CliError(`--tag and --new-tag both give the tag name "${nameInBothOptions}". Case doesn't matter in tag names. Use --tag for a tag the project has and --new-tag for a tag to create.`);
|
|
1379
|
+
const tagCount = existingTagNames.length + newTagNames.length;
|
|
1380
|
+
if (tagCount > MAXIMUM_CONFIG_TAGS) throw new CliError(`--tag and --new-tag give ${tagCount} tags, but a config can have at most ${MAXIMUM_CONFIG_TAGS}.`);
|
|
1381
|
+
return {
|
|
1382
|
+
existingTagNames,
|
|
1383
|
+
newTagNames
|
|
1384
|
+
};
|
|
1385
|
+
};
|
|
1386
|
+
const createNewTag = async (client, project, name) => {
|
|
1387
|
+
try {
|
|
1388
|
+
return await client.createTag(project.id, name);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
if (error instanceof ApiValidationError) throw new CliError(`${error.message}\nTo assign a tag the project already has, use --tag instead of --new-tag.`);
|
|
1391
|
+
throw error;
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
const CREATED_TAGS_LEFT_UNASSIGNED = {
|
|
1395
|
+
created: false,
|
|
1396
|
+
"key-taken": true,
|
|
1397
|
+
failed: true
|
|
1398
|
+
};
|
|
1399
|
+
const unassignedTagsNoticeOf = (createdTags, ending) => {
|
|
1400
|
+
const [firstTag, ...laterTags] = createdTags;
|
|
1401
|
+
return firstTag !== void 0 && CREATED_TAGS_LEFT_UNASSIGNED[ending] ? {
|
|
1402
|
+
form: "tags-left-unassigned",
|
|
1403
|
+
tags: [firstTag, ...laterTags]
|
|
1404
|
+
} : { form: "none" };
|
|
1405
|
+
};
|
|
1406
|
+
const quotedTagName = (tag) => `"${tag.name}"`;
|
|
1407
|
+
const quotedTagNames = ([firstTag, ...laterTags]) => {
|
|
1408
|
+
const lastTag = laterTags.at(-1);
|
|
1409
|
+
return lastTag === void 0 ? quotedTagName(firstTag) : `${[firstTag, ...laterTags.slice(0, -1)].map(quotedTagName).join(", ")} and ${quotedTagName(lastTag)}`;
|
|
1410
|
+
};
|
|
1411
|
+
const unassignedTagsText = (project, tags) => {
|
|
1412
|
+
const count = tags.length;
|
|
1413
|
+
return `Created the ${plural(count, "tag", "tags")} ${quotedTagNames(tags)}. ${plural(count, "It stays", "They stay")} in ${project.slug}'s tag catalog, so pass ${plural(count, "it", "them")} with --tag next time.`;
|
|
1414
|
+
};
|
|
1415
|
+
const writeUnassignedTagsNotice = (context, project, createdTags, ending) => {
|
|
1416
|
+
const notice = unassignedTagsNoticeOf(createdTags, ending);
|
|
1417
|
+
switch (notice.form) {
|
|
1418
|
+
case "none": return;
|
|
1419
|
+
case "tags-left-unassigned":
|
|
1420
|
+
context.invocation.stderr.write(`${unassignedTagsText(project, notice.tags)}\n`);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1324
1424
|
const createConfig = async (context, _commandArguments, options) => {
|
|
1325
1425
|
const newConfig = await newConfigOf(options, context.invocation.workingDirectory);
|
|
1426
|
+
const tagNames = configTagNamesOf(options);
|
|
1326
1427
|
const { client, project } = await connectToLinkedProject(context);
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1428
|
+
const existingTagIds = await tagIdsNamed(client, project, tagNames.existingTagNames);
|
|
1429
|
+
const createdTags = [];
|
|
1430
|
+
try {
|
|
1431
|
+
for (const name of tagNames.newTagNames) createdTags.push(await createNewTag(client, project, name));
|
|
1432
|
+
const creation = await requestCreation(client, project, {
|
|
1433
|
+
...newConfig,
|
|
1434
|
+
tagIds: [...existingTagIds, ...createdTags.map((tag) => tag.id)]
|
|
1435
|
+
});
|
|
1436
|
+
writeUnassignedTagsNotice(context, project, createdTags, creation.outcome);
|
|
1437
|
+
return creationOutput(project, newConfig, creation);
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
writeUnassignedTagsNotice(context, project, createdTags, "failed");
|
|
1440
|
+
throw error;
|
|
1441
|
+
}
|
|
1331
1442
|
};
|
|
1332
1443
|
const archiveConfig = async (context, [configKey]) => {
|
|
1333
1444
|
const { client, project } = await connectToLinkedProject(context);
|
|
@@ -1777,9 +1888,6 @@ const listProjects = async (context) => {
|
|
|
1777
1888
|
};
|
|
1778
1889
|
};
|
|
1779
1890
|
//#endregion
|
|
1780
|
-
//#region src/plural.ts
|
|
1781
|
-
const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
|
|
1782
|
-
//#endregion
|
|
1783
1891
|
//#region src/scanned-repository/short-commit.ts
|
|
1784
1892
|
const SHORT_COMMIT_LENGTH = 7;
|
|
1785
1893
|
const shortCommit = (commit) => commit.slice(0, SHORT_COMMIT_LENGTH);
|
|
@@ -3257,7 +3365,7 @@ const sdkFamilies = {
|
|
|
3257
3365
|
"nuxt-server": {
|
|
3258
3366
|
sdkType: "server",
|
|
3259
3367
|
extensions: javascriptExtensions,
|
|
3260
|
-
importMarkers: ["useConfigDirectorClient"],
|
|
3368
|
+
importMarkers: ["useConfigDirectorServerClient", "useConfigDirectorClient"],
|
|
3261
3369
|
projectArea: "nuxt-server",
|
|
3262
3370
|
calls: javascriptClientCalls
|
|
3263
3371
|
},
|
|
@@ -4440,6 +4548,42 @@ const scan = async (context, [path], options) => {
|
|
|
4440
4548
|
};
|
|
4441
4549
|
};
|
|
4442
4550
|
//#endregion
|
|
4551
|
+
//#region src/request-file.ts
|
|
4552
|
+
const STANDARD_INPUT_ARGUMENT = "-";
|
|
4553
|
+
const readRequestSource = async (argumentLabel, argument, invocation) => {
|
|
4554
|
+
if (argument === STANDARD_INPUT_ARGUMENT) return {
|
|
4555
|
+
origin: `${argumentLabel} reads stdin`,
|
|
4556
|
+
contents: await text(invocation.stdin)
|
|
4557
|
+
};
|
|
4558
|
+
const { file, contents } = await readOptionFile(argumentLabel, argument, invocation.workingDirectory);
|
|
4559
|
+
return {
|
|
4560
|
+
origin: `${argumentLabel} names ${file}`,
|
|
4561
|
+
contents
|
|
4562
|
+
};
|
|
4563
|
+
};
|
|
4564
|
+
const readRequestFile = async (argumentLabel, argument, invocation, expectedFormatSentence) => {
|
|
4565
|
+
const { origin, contents } = await readRequestSource(argumentLabel, argument, invocation);
|
|
4566
|
+
const request = parseJson(contents);
|
|
4567
|
+
if (!isJsonObject(request)) throw new CliError(`${origin}, which does not hold a JSON object. ${expectedFormatSentence}`);
|
|
4568
|
+
return request;
|
|
4569
|
+
};
|
|
4570
|
+
const withId = (entry) => entry.id === void 0 ? {
|
|
4571
|
+
id: randomUUID(),
|
|
4572
|
+
...entry
|
|
4573
|
+
} : entry;
|
|
4574
|
+
//#endregion
|
|
4575
|
+
//#region src/segments/segment-file.ts
|
|
4576
|
+
const readSegmentFile = (segmentArgument, invocation) => readRequestFile("--segment", segmentArgument, invocation, "It takes {\"key\": ..., \"name\": ..., \"groups\": [...]}, like the output of segments get --json.");
|
|
4577
|
+
const conditionWithId = (condition) => isJsonObject(condition) ? withId(condition) : condition;
|
|
4578
|
+
const conditionGroupWithIds = (group) => Array.isArray(group) ? group.map(conditionWithId) : group;
|
|
4579
|
+
const conditionGroupsWithIds = (groups) => groups.map(conditionGroupWithIds);
|
|
4580
|
+
const overridesWithIds = (overrides) => Object.fromEntries(Object.entries(overrides).map(([environmentId, groups]) => [environmentId, Array.isArray(groups) ? conditionGroupsWithIds(groups) : groups]));
|
|
4581
|
+
const withGeneratedConditionIds = (segment) => ({
|
|
4582
|
+
...segment,
|
|
4583
|
+
...Array.isArray(segment.groups) ? { groups: conditionGroupsWithIds(segment.groups) } : {},
|
|
4584
|
+
...isJsonObject(segment.overrides) ? { overrides: overridesWithIds(segment.overrides) } : {}
|
|
4585
|
+
});
|
|
4586
|
+
//#endregion
|
|
4443
4587
|
//#region src/segments/segment-text.ts
|
|
4444
4588
|
const conditionGroupLines = (heading, groups) => [
|
|
4445
4589
|
"",
|
|
@@ -4470,22 +4614,72 @@ const listSegments = async (context) => {
|
|
|
4470
4614
|
exitCode: ExitCode.success
|
|
4471
4615
|
};
|
|
4472
4616
|
};
|
|
4473
|
-
const
|
|
4474
|
-
if (Object.keys(segment.overrides).length === 0) return /* @__PURE__ */ new Map();
|
|
4617
|
+
const environmentSlugsById = async (client, project) => {
|
|
4475
4618
|
const environments = await ifPermitted(client.listEnvironments(project.id));
|
|
4476
4619
|
return new Map((environments?.parsedBody ?? []).map((environment) => [environment.id, environment.slug]));
|
|
4477
4620
|
};
|
|
4621
|
+
const overrideEnvironmentSlugsOf = async (client, project, segment) => Object.keys(segment.overrides).length === 0 ? /* @__PURE__ */ new Map() : environmentSlugsById(client, project);
|
|
4622
|
+
const noSegmentWithKey = (project, segmentKey) => new CliError(`No segment in ${project.slug} has the key "${segmentKey}".`);
|
|
4478
4623
|
const getSegment = async (context, [segmentKey]) => {
|
|
4479
4624
|
const { client, project } = await connectToLinkedProject(context);
|
|
4480
4625
|
const receivedSegment = await client.getSegment(project.id, segmentKey);
|
|
4481
|
-
if (receivedSegment === void 0) throw
|
|
4626
|
+
if (receivedSegment === void 0) throw noSegmentWithKey(project, segmentKey);
|
|
4482
4627
|
const { parsedBody: segment, responseBody } = receivedSegment;
|
|
4483
4628
|
return {
|
|
4484
4629
|
json: responseBody,
|
|
4485
|
-
text: segmentText(segment, await
|
|
4630
|
+
text: segmentText(segment, await overrideEnvironmentSlugsOf(client, project, segment)),
|
|
4631
|
+
exitCode: ExitCode.success
|
|
4632
|
+
};
|
|
4633
|
+
};
|
|
4634
|
+
const createSegment = async (context, _commandArguments, options) => {
|
|
4635
|
+
const requestedSegment = withGeneratedConditionIds(await readSegmentFile(options.segment, context.invocation));
|
|
4636
|
+
const { client, project } = await connectToLinkedProject(context);
|
|
4637
|
+
const { parsedBody: segment, responseBody } = await client.createSegment(project.id, requestedSegment);
|
|
4638
|
+
return {
|
|
4639
|
+
json: responseBody,
|
|
4640
|
+
text: [`Created segment ${segment.key} in ${project.slug}.`, segmentText(segment, await overrideEnvironmentSlugsOf(client, project, segment))].join("\n"),
|
|
4641
|
+
exitCode: ExitCode.success
|
|
4642
|
+
};
|
|
4643
|
+
};
|
|
4644
|
+
const updateSegment = async (context, [segmentKey], options) => {
|
|
4645
|
+
const requestedSegment = withGeneratedConditionIds(await readSegmentFile(options.segment, context.invocation));
|
|
4646
|
+
const { client, project } = await connectToLinkedProject(context);
|
|
4647
|
+
const currentSegment = await client.getSegment(project.id, segmentKey);
|
|
4648
|
+
if (currentSegment === void 0) throw noSegmentWithKey(project, segmentKey);
|
|
4649
|
+
const updatedSegment = await client.updateSegment(project.id, segmentKey, {
|
|
4650
|
+
...requestedSegment,
|
|
4651
|
+
id: currentSegment.parsedBody.id
|
|
4652
|
+
});
|
|
4653
|
+
if (updatedSegment === void 0) throw noSegmentWithKey(project, segmentKey);
|
|
4654
|
+
const { parsedBody: segment, responseBody } = updatedSegment;
|
|
4655
|
+
return {
|
|
4656
|
+
json: responseBody,
|
|
4657
|
+
text: [`Updated segment ${segmentKey} in ${project.slug}.`, segmentText(segment, await overrideEnvironmentSlugsOf(client, project, segment))].join("\n"),
|
|
4486
4658
|
exitCode: ExitCode.success
|
|
4487
4659
|
};
|
|
4488
4660
|
};
|
|
4661
|
+
const usageLine = (usage, environmentSlugs) => {
|
|
4662
|
+
const environmentNames = usage.environmentIds.map((environmentId) => environmentSlugs.get(environmentId) ?? environmentId);
|
|
4663
|
+
return ` ${usage.configKey} in ${environmentNames.join(", ")}`;
|
|
4664
|
+
};
|
|
4665
|
+
const segmentInUseError = (segmentKey, usages, environmentSlugs) => new CliError([
|
|
4666
|
+
`The targeting rules of ${usages.length} ${plural(usages.length, "config use", "configs use")} the segment ${segmentKey}, so it was not deleted:`,
|
|
4667
|
+
...usages.map((usage) => usageLine(usage, environmentSlugs)),
|
|
4668
|
+
"Remove those segment conditions first, with configdirector targeting-rules set."
|
|
4669
|
+
].join("\n"));
|
|
4670
|
+
const deleteSegment = async (context, [segmentKey]) => {
|
|
4671
|
+
const { client, project } = await connectToLinkedProject(context);
|
|
4672
|
+
const deletion = await client.deleteSegment(project.id, segmentKey);
|
|
4673
|
+
switch (deletion.outcome) {
|
|
4674
|
+
case "deleted": return {
|
|
4675
|
+
json: { deletedSegmentKey: segmentKey },
|
|
4676
|
+
text: [`Deleted segment ${segmentKey} from ${project.slug}.`, ""].join("\n"),
|
|
4677
|
+
exitCode: ExitCode.success
|
|
4678
|
+
};
|
|
4679
|
+
case "not-found": throw noSegmentWithKey(project, segmentKey);
|
|
4680
|
+
case "in-use": throw segmentInUseError(segmentKey, deletion.usages, await environmentSlugsById(client, project));
|
|
4681
|
+
}
|
|
4682
|
+
};
|
|
4489
4683
|
//#endregion
|
|
4490
4684
|
//#region src/targeting-rules/reference-text.ts
|
|
4491
4685
|
const headedListLines = (heading, items) => [heading, ...items.map((item) => ` ${item}`)];
|
|
@@ -4505,28 +4699,7 @@ const targetingRulesReferenceText = (reference) => [
|
|
|
4505
4699
|
].join("\n");
|
|
4506
4700
|
//#endregion
|
|
4507
4701
|
//#region src/targeting-rules/targeting-rules-file.ts
|
|
4508
|
-
const
|
|
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;
|
|
4702
|
+
const readTargetingRulesFile = (rulesArgument, invocation) => readRequestFile("--rules", rulesArgument, invocation, "It takes {\"defaultValue\": ..., \"rules\": [...]}, like an entry of targets in configs get --json.");
|
|
4530
4703
|
const conditionOrPercentageWithId = (conditionOrPercentage) => isJsonObject(conditionOrPercentage) ? withId(conditionOrPercentage) : conditionOrPercentage;
|
|
4531
4704
|
const conditionsOrPercentagesWithIds = (rule, field) => {
|
|
4532
4705
|
const conditionsOrPercentages = rule[field];
|
|
@@ -4784,6 +4957,7 @@ const validate = async (context, [path]) => {
|
|
|
4784
4957
|
//#endregion
|
|
4785
4958
|
//#region src/run.ts
|
|
4786
4959
|
const DEFAULT_BASE_URL = "https://api.configdirector.com";
|
|
4960
|
+
const SEGMENT_FILE_DESCRIPTION = "a JSON file holding the segment as the Admin API takes it, or - to read it from stdin (required)";
|
|
4787
4961
|
const withoutTrailingSlashes = (url) => {
|
|
4788
4962
|
let trimmedUrl = url;
|
|
4789
4963
|
while (trimmedUrl.endsWith("/")) trimmedUrl = trimmedUrl.slice(0, -1);
|
|
@@ -4834,11 +5008,14 @@ const run = async (invocation) => {
|
|
|
4834
5008
|
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)).action(commandAction(listConfigs));
|
|
4835
5009
|
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));
|
|
4836
5010
|
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
|
-
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));
|
|
5011
|
+
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("--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));
|
|
4838
5012
|
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
|
|
5013
|
+
const segmentsCommand = program.command("segments").description("list, show, create, update and delete a project's segments");
|
|
4840
5014
|
segmentsCommand.command("list").description("list the project's segments with how many configs use each").action(commandAction(listSegments));
|
|
4841
5015
|
segmentsCommand.command("get").argument("<key>", "the segment's key").description("show a segment's condition groups and its environment overrides").action(commandAction(getSegment));
|
|
5016
|
+
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));
|
|
5017
|
+
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));
|
|
5018
|
+
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
5019
|
const targetingRulesCommand = program.command("targeting-rules").description("replace a config's targeting rules in one environment and show the format they take");
|
|
4843
5020
|
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
5021
|
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