@configdirector/cli 0.17.0 → 0.18.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 +2 -0
- package/dist/configdirector.mjs +38 -18
- 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.18.0] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `configdirector configs get`, `evaluate` and `configs create` name an experiment's variations wherever they show one of its values, with the value after the name, as in `40% get control (Start free trial), 60% get treatment-a (Try it free for 14 days)` and `Default targeting rule: control (Start free trial)`. `evaluate` names the variation it served too, as in `treatment-a (Try it free for 14 days), served by rule 2`. A variation without a name, and a config without variations, show the value alone, as before. `--json` prints the Admin API's response as received, which now carries the id of each value's variation.
|
|
12
|
+
|
|
7
13
|
## [0.17.0] - 2026-11-01
|
|
8
14
|
|
|
9
15
|
### Added
|
package/README.md
CHANGED
|
@@ -211,6 +211,8 @@ production (live)
|
|
|
211
211
|
Default targeting rule: false
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
On an experiment, each value comes after the name of its variation, as in `40% get control (Start free trial), 60% get treatment-a (Try it free for 14 days)`. `evaluate` names the variation it served the same way, as in `treatment-a (Try it free for 14 days), served by rule 2`, and `configs create` names the variation of the default targeting rule value it starts with.
|
|
215
|
+
|
|
214
216
|
`projects list`, `environments list`, `configs list`, `configs activity` and `segments list` print a header row that names their columns above their lines, such as `KEY TYPE ROLE LIFETIME STATE` for `configs list`, and `--no-headers` leaves it out. An empty list prints only a sentence, such as `No configs found in web-app.`
|
|
215
217
|
|
|
216
218
|
With `--json`, each prints the Admin API's response as received, except `configs list --state`, which prints only the configs in that state. A key no config or segment has exits with 2.
|
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.18.0";
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/CliError.ts
|
|
18
18
|
var CliError = class extends Error {
|
|
@@ -154,14 +154,16 @@ const ConditionSchema = v.variant("kind", [v.object({
|
|
|
154
154
|
}), SegmentConditionSchema]);
|
|
155
155
|
const PercentageSchema = v.object({
|
|
156
156
|
percentage: v.number(),
|
|
157
|
-
value: RuleValueSchema
|
|
157
|
+
value: RuleValueSchema,
|
|
158
|
+
variationId: v.optional(v.string())
|
|
158
159
|
});
|
|
159
160
|
const RuleSchema = v.variant("type", [v.variant("target", [v.object({
|
|
160
161
|
id: v.string(),
|
|
161
162
|
type: v.literal("conditional"),
|
|
162
163
|
conditions: v.array(ConditionSchema),
|
|
163
164
|
target: v.literal("value"),
|
|
164
|
-
value: RuleValueSchema
|
|
165
|
+
value: RuleValueSchema,
|
|
166
|
+
variationId: v.optional(v.string())
|
|
165
167
|
}), v.object({
|
|
166
168
|
id: v.string(),
|
|
167
169
|
type: v.literal("conditional"),
|
|
@@ -174,6 +176,12 @@ const RuleSchema = v.variant("type", [v.variant("target", [v.object({
|
|
|
174
176
|
percentages: v.array(PercentageSchema)
|
|
175
177
|
})]);
|
|
176
178
|
//#endregion
|
|
179
|
+
//#region src/admin-api/variations.ts
|
|
180
|
+
const VariationSchema = v.object({
|
|
181
|
+
id: v.string(),
|
|
182
|
+
name: v.nullish(v.string())
|
|
183
|
+
});
|
|
184
|
+
//#endregion
|
|
177
185
|
//#region src/admin-api/config-with-targeting-rules.ts
|
|
178
186
|
const TargetingRulesInEnvironmentSchema = v.object({
|
|
179
187
|
environment: v.object({
|
|
@@ -181,7 +189,8 @@ const TargetingRulesInEnvironmentSchema = v.object({
|
|
|
181
189
|
live: v.boolean()
|
|
182
190
|
}),
|
|
183
191
|
rules: v.array(RuleSchema),
|
|
184
|
-
defaultValue: v.string()
|
|
192
|
+
defaultValue: v.string(),
|
|
193
|
+
defaultVariationId: v.optional(v.string())
|
|
185
194
|
});
|
|
186
195
|
const NumericBoundSchema = v.object({
|
|
187
196
|
relation: v.string(),
|
|
@@ -239,6 +248,7 @@ const ConfigWithTargetingRulesSchema = v.intersect([v.object({
|
|
|
239
248
|
client: v.boolean(),
|
|
240
249
|
server: v.boolean(),
|
|
241
250
|
tags: v.array(v.object({ name: v.string() })),
|
|
251
|
+
variations: v.array(VariationSchema),
|
|
242
252
|
targets: v.array(TargetingRulesInEnvironmentSchema)
|
|
243
253
|
}), ConfigTypeSchema]);
|
|
244
254
|
//#endregion
|
|
@@ -348,6 +358,7 @@ const ConfigEvaluationSchema = v.object({
|
|
|
348
358
|
identifier: v.nullable(v.string())
|
|
349
359
|
})),
|
|
350
360
|
value: v.optional(v.string()),
|
|
361
|
+
variationId: v.optional(v.string()),
|
|
351
362
|
servedBy: v.variant("kind", [v.object({
|
|
352
363
|
kind: v.literal("rule"),
|
|
353
364
|
ruleId: v.string()
|
|
@@ -355,9 +366,11 @@ const ConfigEvaluationSchema = v.object({
|
|
|
355
366
|
rules: v.array(RuleExplanationSchema),
|
|
356
367
|
targetingRules: v.object({
|
|
357
368
|
rules: v.array(RuleSchema),
|
|
358
|
-
defaultValue: v.string()
|
|
369
|
+
defaultValue: v.string(),
|
|
370
|
+
defaultVariationId: v.optional(v.string())
|
|
359
371
|
}),
|
|
360
|
-
segments: v.record(v.string(), v.object({ name: v.string() }))
|
|
372
|
+
segments: v.record(v.string(), v.object({ name: v.string() })),
|
|
373
|
+
variations: v.optional(v.array(VariationSchema), [])
|
|
361
374
|
});
|
|
362
375
|
//#endregion
|
|
363
376
|
//#region src/admin-api/AdminApiClient.ts
|
|
@@ -961,10 +974,12 @@ const describeCondition = (condition, segments) => {
|
|
|
961
974
|
case "segment": return `${condition.operator} segment ${segmentNameOf(condition.segmentId, segments)}`;
|
|
962
975
|
}
|
|
963
976
|
};
|
|
964
|
-
const
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
977
|
+
const valueWithVariationName = (value, variationName) => variationName ? `${variationName} (${String(value)})` : String(value);
|
|
978
|
+
const describeValue = (value, variationId, variations) => valueWithVariationName(value, variations.find((variation) => variation.id === variationId)?.name);
|
|
979
|
+
const describePercentages = (percentages, variations) => percentages.map((percentage) => `${formatPercentage(percentage.percentage)}% get ${describeValue(percentage.value, percentage.variationId, variations)}`).join(", ");
|
|
980
|
+
const describeRule = (rule, segments, variations) => {
|
|
981
|
+
if (rule.type === "percentage") return describePercentages(rule.percentages, variations);
|
|
982
|
+
return `${rule.conditions.map((condition) => describeCondition(condition, segments)).join(" and ")}: ${rule.target === "value" ? describeValue(rule.value, rule.variationId, variations) : describePercentages(rule.percentages, variations)}`;
|
|
968
983
|
};
|
|
969
984
|
//#endregion
|
|
970
985
|
//#region src/configs/config-text.ts
|
|
@@ -1001,16 +1016,16 @@ const settingLines = (config) => [
|
|
|
1001
1016
|
...config.tags.length === 0 ? [] : [`Tags: ${config.tags.map((tag) => tag.name).join(", ")}`]
|
|
1002
1017
|
];
|
|
1003
1018
|
const environmentLabel = (environment) => environment.live ? `${environment.slug} (live)` : environment.slug;
|
|
1004
|
-
const targetingRuleLines = (targetingRules, segments) => [
|
|
1019
|
+
const targetingRuleLines = (targetingRules, segments, variations) => [
|
|
1005
1020
|
environmentLabel(targetingRules.environment),
|
|
1006
|
-
...targetingRules.rules.map((rule, index) => ` ${index + 1}. ${describeRule(rule, segments)}`),
|
|
1007
|
-
` Default targeting rule: ${targetingRules.defaultValue}`
|
|
1021
|
+
...targetingRules.rules.map((rule, index) => ` ${index + 1}. ${describeRule(rule, segments, variations)}`),
|
|
1022
|
+
` Default targeting rule: ${describeValue(targetingRules.defaultValue, targetingRules.defaultVariationId, variations)}`
|
|
1008
1023
|
];
|
|
1009
1024
|
const configText = (config, requestedKey, segments) => [
|
|
1010
1025
|
...oldKeyLines(requestedKey, config.key),
|
|
1011
1026
|
config.key,
|
|
1012
1027
|
...settingLines(config).map((line) => ` ${line}`),
|
|
1013
|
-
...config.targets.flatMap((targetingRules) => ["", ...targetingRuleLines(targetingRules, segments)]),
|
|
1028
|
+
...config.targets.flatMap((targetingRules) => ["", ...targetingRuleLines(targetingRules, segments, config.variations)]),
|
|
1014
1029
|
""
|
|
1015
1030
|
].join("\n");
|
|
1016
1031
|
//#endregion
|
|
@@ -1464,6 +1479,10 @@ const getConfig = async (context, [configKey]) => {
|
|
|
1464
1479
|
exitCode: ExitCode.success
|
|
1465
1480
|
};
|
|
1466
1481
|
};
|
|
1482
|
+
const defaultValueDescription = (newConfig) => {
|
|
1483
|
+
const defaultVariation = newConfig.variations.find((variation) => variation.value === newConfig.defaultValue);
|
|
1484
|
+
return valueWithVariationName(newConfig.defaultValue, defaultVariation?.name);
|
|
1485
|
+
};
|
|
1467
1486
|
const createdConfigLines = (projectSlug, createdConfig, newConfig) => {
|
|
1468
1487
|
const settings = [
|
|
1469
1488
|
createdConfig.type,
|
|
@@ -1471,7 +1490,7 @@ const createdConfigLines = (projectSlug, createdConfig, newConfig) => {
|
|
|
1471
1490
|
createdConfig.lifetime,
|
|
1472
1491
|
availabilityDescription(createdConfig)
|
|
1473
1492
|
];
|
|
1474
|
-
return [`Created ${createdConfig.key} in ${projectSlug}: ${settings.join(", ")}.`, `Default targeting rule in every environment: ${
|
|
1493
|
+
return [`Created ${createdConfig.key} in ${projectSlug}: ${settings.join(", ")}.`, `Default targeting rule in every environment: ${defaultValueDescription(newConfig)}`];
|
|
1475
1494
|
};
|
|
1476
1495
|
const keyTakenLines = (projectSlug, configKey, existingConfig) => [`${projectSlug} already has a config with the key ${configKey}, so no config was created:`, ...alignedColumns([summaryColumnsOf(existingConfig)])];
|
|
1477
1496
|
const rejectsKey = (error) => error instanceof ApiValidationError && error.fieldErrors.some((fieldError) => fieldError.name === "key");
|
|
@@ -1766,6 +1785,7 @@ const contextDescription = (evaluation, contextForm) => {
|
|
|
1766
1785
|
const identity = `${evaluatedContext.kind} ${evaluatedContext.identifier}`;
|
|
1767
1786
|
return contextForm.form === "saved" ? `saved context ${identity}` : identity;
|
|
1768
1787
|
};
|
|
1788
|
+
const servedValueDescription = (evaluation) => evaluation.value === void 0 ? "no value" : describeValue(evaluation.value, evaluation.variationId, evaluation.variations);
|
|
1769
1789
|
const servedByDescription = (evaluation) => {
|
|
1770
1790
|
const { servedBy } = evaluation;
|
|
1771
1791
|
switch (servedBy.kind) {
|
|
@@ -1813,17 +1833,17 @@ const ruleLines = (evaluation) => {
|
|
|
1813
1833
|
const rulesById = new Map(evaluation.targetingRules.rules.map((rule) => [rule.id, rule]));
|
|
1814
1834
|
return alignedColumns(evaluation.rules.map((explanation, index) => {
|
|
1815
1835
|
const rule = rulesById.get(explanation.ruleId);
|
|
1816
|
-
return [`${index + 1}. ${rule === void 0 ? explanation.ruleId : describeRule(rule, evaluation.segments)}`, RULE_OUTCOME_DESCRIPTIONS[explanation.outcome](explanation, conditionsOf(rule), evaluation.segments)];
|
|
1836
|
+
return [`${index + 1}. ${rule === void 0 ? explanation.ruleId : describeRule(rule, evaluation.segments, evaluation.variations)}`, RULE_OUTCOME_DESCRIPTIONS[explanation.outcome](explanation, conditionsOf(rule), evaluation.segments)];
|
|
1817
1837
|
}));
|
|
1818
1838
|
};
|
|
1819
1839
|
const evaluationText = (evaluation, requested) => [
|
|
1820
1840
|
...oldKeyLines(requested.configKey, evaluation.key),
|
|
1821
1841
|
`${evaluation.key} in ${evaluation.environment.name} for ${contextDescription(evaluation, requested.contextForm)}`,
|
|
1822
1842
|
"",
|
|
1823
|
-
`${evaluation
|
|
1843
|
+
`${servedValueDescription(evaluation)}, ${servedByDescription(evaluation)}`,
|
|
1824
1844
|
"",
|
|
1825
1845
|
...ruleLines(evaluation),
|
|
1826
|
-
`Default targeting rule: ${evaluation.targetingRules.defaultValue}`,
|
|
1846
|
+
`Default targeting rule: ${describeValue(evaluation.targetingRules.defaultValue, evaluation.targetingRules.defaultVariationId, evaluation.variations)}`,
|
|
1827
1847
|
""
|
|
1828
1848
|
].join("\n");
|
|
1829
1849
|
//#endregion
|
package/package.json
CHANGED