@configdirector/cli 0.12.0 → 0.14.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 +17 -0
- package/README.md +11 -2
- package/dist/configdirector.mjs +86 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `@configdirector/cli` are documented here. The format fol
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.14.0] - 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `configdirector scan` and `validate` find configs read through `evaluate`, which the JavaScript SDKs added in `@configdirector/client-sdk`, `react-web-sdk`, `vue-sdk` and `react-native-sdk` 1.10.0, `server-sdk` 1.12.0, and `nextjs-sdk` and `nuxt-sdk` 1.13.0. A call such as `client.evaluate("new-checkout", false)` is reported and checked as `client.getValue("new-checkout", false)` is, with its in-code default value, so `validate` fails on a wrong key or type there too.
|
|
12
|
+
|
|
13
|
+
## [0.13.0] - 2026-10-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `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.
|
|
18
|
+
- `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.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `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.
|
|
23
|
+
|
|
7
24
|
## [0.12.0] - 2026-10-19
|
|
8
25
|
|
|
9
26
|
### Added
|
package/README.md
CHANGED
|
@@ -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
|
|
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.14.0";
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/CliError.ts
|
|
18
18
|
var CliError = class extends Error {
|
|
@@ -395,7 +395,10 @@ const ConfigActivitySchema = v.object({ latestEvaluations: v.array(v.object({
|
|
|
395
395
|
sdkVersion: v.nullable(v.string()),
|
|
396
396
|
time: v.pipe(v.string(), v.isoTimestamp())
|
|
397
397
|
})) });
|
|
398
|
-
const TagSchema = v.object({
|
|
398
|
+
const TagSchema = v.object({
|
|
399
|
+
id: v.string(),
|
|
400
|
+
name: v.string()
|
|
401
|
+
});
|
|
399
402
|
const tagFilterQuery = (tagFilter) => {
|
|
400
403
|
switch (tagFilter.form) {
|
|
401
404
|
case "every-config": return "";
|
|
@@ -505,6 +508,12 @@ var AdminApiClient = class {
|
|
|
505
508
|
async findTagByName(projectId, name) {
|
|
506
509
|
return (await this.getIfFound(`/v1/projects/${encodeURIComponent(projectId)}/tags/by-name?${new URLSearchParams({ name })}`, TagSchema))?.parsedBody;
|
|
507
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
|
+
}
|
|
508
517
|
createConfig(projectId, request) {
|
|
509
518
|
return this.requestJson(`/v1/projects/${encodeURIComponent(projectId)}/configs`, CreatedConfigSchema, {
|
|
510
519
|
method: "POST",
|
|
@@ -1202,6 +1211,9 @@ const newConfigOf = async (options, workingDirectory) => {
|
|
|
1202
1211
|
};
|
|
1203
1212
|
};
|
|
1204
1213
|
//#endregion
|
|
1214
|
+
//#region src/plural.ts
|
|
1215
|
+
const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
|
|
1216
|
+
//#endregion
|
|
1205
1217
|
//#region src/project-selection.ts
|
|
1206
1218
|
const requireProjectSlug = (context) => {
|
|
1207
1219
|
if (context.projectSlug === void 0) throw new CliError("Name the project with --project <slug> or CONFIGDIRECTOR_PROJECT.");
|
|
@@ -1355,13 +1367,78 @@ const creationOutput = (project, newConfig, creation) => {
|
|
|
1355
1367
|
};
|
|
1356
1368
|
}
|
|
1357
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
|
+
};
|
|
1358
1424
|
const createConfig = async (context, _commandArguments, options) => {
|
|
1359
1425
|
const newConfig = await newConfigOf(options, context.invocation.workingDirectory);
|
|
1426
|
+
const tagNames = configTagNamesOf(options);
|
|
1360
1427
|
const { client, project } = await connectToLinkedProject(context);
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
+
}
|
|
1365
1442
|
};
|
|
1366
1443
|
const archiveConfig = async (context, [configKey]) => {
|
|
1367
1444
|
const { client, project } = await connectToLinkedProject(context);
|
|
@@ -1811,9 +1888,6 @@ const listProjects = async (context) => {
|
|
|
1811
1888
|
};
|
|
1812
1889
|
};
|
|
1813
1890
|
//#endregion
|
|
1814
|
-
//#region src/plural.ts
|
|
1815
|
-
const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
|
|
1816
|
-
//#endregion
|
|
1817
1891
|
//#region src/scanned-repository/short-commit.ts
|
|
1818
1892
|
const SHORT_COMMIT_LENGTH = 7;
|
|
1819
1893
|
const shortCommit = (commit) => commit.slice(0, SHORT_COMMIT_LENGTH);
|
|
@@ -3152,6 +3226,7 @@ const testAnnotation = (name) => ({
|
|
|
3152
3226
|
});
|
|
3153
3227
|
const javascriptClientCalls = [
|
|
3154
3228
|
read("getValue"),
|
|
3229
|
+
read("evaluate"),
|
|
3155
3230
|
read("watch"),
|
|
3156
3231
|
keyOnly("unwatch")
|
|
3157
3232
|
];
|
|
@@ -3291,7 +3366,7 @@ const sdkFamilies = {
|
|
|
3291
3366
|
"nuxt-server": {
|
|
3292
3367
|
sdkType: "server",
|
|
3293
3368
|
extensions: javascriptExtensions,
|
|
3294
|
-
importMarkers: ["useConfigDirectorClient"],
|
|
3369
|
+
importMarkers: ["useConfigDirectorServerClient", "useConfigDirectorClient"],
|
|
3295
3370
|
projectArea: "nuxt-server",
|
|
3296
3371
|
calls: javascriptClientCalls
|
|
3297
3372
|
},
|
|
@@ -4934,7 +5009,7 @@ const run = async (invocation) => {
|
|
|
4934
5009
|
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));
|
|
4935
5010
|
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));
|
|
4936
5011
|
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));
|
|
4937
|
-
configsCommand.command("create").description("create a config from options that mirror the Admin API's fields").requiredOption("--key <key>", "the config's key (required)").option("--description <text>", "a description shown with the config in the dashboard").addOption(new Option("--type <type>", "the type of the config's values (required)").choices(CONFIG_TYPES).makeOptionMandatory()).addOption(new Option("--role <role>", "what the config is for (required)").choices(CONFIG_ROLES).makeOptionMandatory()).addOption(new Option("--lifetime <lifetime>", "whether the config is expected to be removed (required)").choices(CONFIG_LIFETIMES).makeOptionMandatory()).requiredOption("--default-value <value>", "the default targeting rule value every environment starts with, in the type given by --type; not the in-code default value an application passes to the SDK (required)").option("--client-only", "serve the config to client SDKs only (default: client and server SDKs)").addOption(new Option("--server-only", "serve the config to server SDKs only (default: client and server SDKs)").conflicts("clientOnly")).option("--tag <name>", "assign the project's tag with this name; repeatable", repeatedOption).option("--variation <name=value>", "a variation of an experiment, its value in the type given by --type; repeatable", repeatedOption).addOption(new Option("--min <number>", "the smallest value allowed, included").helpGroup(typeConstraintHelpGroup("min"))).addOption(new Option("--max <number>", "the largest value allowed, included").helpGroup(typeConstraintHelpGroup("max"))).addOption(new Option("--exclusive-min <number>", "values must be greater than this number").conflicts("min").helpGroup(typeConstraintHelpGroup("exclusiveMin"))).addOption(new Option("--exclusive-max <number>", "values must be less than this number").conflicts("max").helpGroup(typeConstraintHelpGroup("exclusiveMax"))).addOption(new Option("--unit <unit>", "the unit of the number the SDKs receive (required)").choices(TIMESPAN_UNITS).helpGroup(typeConstraintHelpGroup("unit"))).addOption(new Option("--allow-fractions", "allow values with a fraction of the unit").helpGroup(typeConstraintHelpGroup("allowFractions"))).addOption(new Option("--allowed-value <value>", "a value the config can take; repeatable").argParser(repeatedOption).helpGroup(typeConstraintHelpGroup("allowedValue"))).addOption(new Option("--enum-value-type <type>", "whether the allowed values are strings or numbers (default: string)").choices(ENUM_VALUE_TYPES).helpGroup(typeConstraintHelpGroup("enumValueType"))).addOption(new Option("--https-only", "allow only HTTPS URLs").helpGroup(typeConstraintHelpGroup("httpsOnly"))).addOption(new Option("--json-schema <file>", "a file holding the JSON Schema every value must match").helpGroup(typeConstraintHelpGroup("jsonSchema"))).action(commandAction(createConfig));
|
|
5012
|
+
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));
|
|
4938
5013
|
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));
|
|
4939
5014
|
const segmentsCommand = program.command("segments").description("list, show, create, update and delete a project's segments");
|
|
4940
5015
|
segmentsCommand.command("list").description("list the project's segments with how many configs use each").action(commandAction(listSegments));
|
package/package.json
CHANGED