@twin.org/ts-to-openapi 0.0.1-next.26 → 0.0.1-next.28
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/dist/cjs/index.cjs +83 -21
- package/dist/esm/index.mjs +84 -22
- package/docs/changelog.md +36 -0
- package/package.json +2 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -659,11 +659,13 @@ async function finaliseOutput(usedCommonResponseTypes, schemas, openApi, securit
|
|
|
659
659
|
}
|
|
660
660
|
}
|
|
661
661
|
}
|
|
662
|
-
|
|
663
|
-
|
|
662
|
+
// Remove standard types that we don't want in the final output
|
|
663
|
+
const removeTypes = ["HttpStatusCode", "Uint8Array", "ArrayBuffer"];
|
|
664
|
+
for (const type of removeTypes) {
|
|
665
|
+
delete finalSchemas[type];
|
|
664
666
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
+
for (const type in finalSchemas) {
|
|
668
|
+
processArrays(finalSchemas[type]);
|
|
667
669
|
}
|
|
668
670
|
const schemaKeys = Object.keys(finalSchemas);
|
|
669
671
|
schemaKeys.sort();
|
|
@@ -696,12 +698,7 @@ async function finaliseOutput(usedCommonResponseTypes, schemas, openApi, securit
|
|
|
696
698
|
// Remove the array [] from the type names
|
|
697
699
|
// eslint-disable-next-line unicorn/better-regex
|
|
698
700
|
json = json.replace(/#\/components\/schemas\/(.*)\[\]/g, "#/components/schemas/ListOf$1");
|
|
699
|
-
|
|
700
|
-
json = json.replace(/Partial%3CI(.*?)%3E/g, "$1");
|
|
701
|
-
// Remove the omit markers
|
|
702
|
-
json = json.replace(/Omit%3CI(.*?)%2C.*%3E/g, "$1");
|
|
703
|
-
// Cleanup the generic markers
|
|
704
|
-
json = json.replace(/%3Cunknown%3E/g, "");
|
|
701
|
+
json = normaliseTypeName(json);
|
|
705
702
|
// Remove external references
|
|
706
703
|
for (const finalExternal in finalExternals) {
|
|
707
704
|
json = json.replace(new RegExp(`"#/components/schemas/${core.StringHelper.stripPrefix(finalExternal)}"`, "g"), `"${finalExternals[finalExternal]}"`);
|
|
@@ -855,12 +852,7 @@ async function generateSchemas(modelDirWildcards, types, outputWorkingDir) {
|
|
|
855
852
|
const schema = generator.createSchema("*");
|
|
856
853
|
if (schema.definitions) {
|
|
857
854
|
for (const def in schema.definitions) {
|
|
858
|
-
|
|
859
|
-
let defSub = def.replace(/^Partial<(.*?)>/g, "$1");
|
|
860
|
-
// Remove the omit markers
|
|
861
|
-
defSub = defSub.replace(/^Omit<(.*?),.*>/g, "$1");
|
|
862
|
-
// Cleanup the generic markers
|
|
863
|
-
defSub = defSub.replace(/</g, "%3C").replace(/>/g, "%3E");
|
|
855
|
+
const defSub = normaliseTypeName(def);
|
|
864
856
|
allSchemas[defSub] = schema.definitions[def];
|
|
865
857
|
}
|
|
866
858
|
}
|
|
@@ -902,10 +894,7 @@ function extractTypes(allSchemas, requiredTypes, referencedSchemas) {
|
|
|
902
894
|
function extractTypesFromSchema(allTypes, schema, output) {
|
|
903
895
|
const additionalTypes = [];
|
|
904
896
|
if (core.Is.stringValue(schema.$ref)) {
|
|
905
|
-
additionalTypes.push(schema.$ref
|
|
906
|
-
.replace("#/definitions/", "")
|
|
907
|
-
.replace(/^Partial%3C(.*?)%3E/g, "$1")
|
|
908
|
-
.replace(/^Omit%3C(.*?)%2C.*%3E/g, "$1"));
|
|
897
|
+
additionalTypes.push(normaliseTypeName(schema.$ref).replace("#/definitions/", ""));
|
|
909
898
|
}
|
|
910
899
|
else if (core.Is.object(schema.items)) {
|
|
911
900
|
if (core.Is.arrayValue(schema.items)) {
|
|
@@ -1098,6 +1087,79 @@ async function loadPackages(tsToOpenApiConfig, outputWorkingDir, typeRoots) {
|
|
|
1098
1087
|
}
|
|
1099
1088
|
return restRoutes;
|
|
1100
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Process arrays in the schema object.
|
|
1092
|
+
* @param schemaObject The schema object to process.
|
|
1093
|
+
*/
|
|
1094
|
+
function processArrays(schemaObject) {
|
|
1095
|
+
if (core.Is.object(schemaObject)) {
|
|
1096
|
+
// latest specs have singular items in `items` property
|
|
1097
|
+
// and multiple items in prefixItems, so update the schema accordingly
|
|
1098
|
+
// https://www.learnjsonschema.com/2020-12/applicator/items/
|
|
1099
|
+
// https://www.learnjsonschema.com/2020-12/applicator/prefixitems/
|
|
1100
|
+
const schemaItems = schemaObject.items;
|
|
1101
|
+
if (core.Is.array(schemaItems) || core.Is.object(schemaItems)) {
|
|
1102
|
+
schemaObject.prefixItems = core.ArrayHelper.fromObjectOrArray(schemaItems);
|
|
1103
|
+
delete schemaObject.items;
|
|
1104
|
+
}
|
|
1105
|
+
const additionalItems = schemaObject.additionalItems;
|
|
1106
|
+
if (core.Is.array(additionalItems) || core.Is.object(additionalItems)) {
|
|
1107
|
+
schemaObject.items = core.ArrayHelper.fromObjectOrArray(additionalItems)[0];
|
|
1108
|
+
delete schemaObject.additionalItems;
|
|
1109
|
+
}
|
|
1110
|
+
processSchemaDictionary(schemaObject.properties);
|
|
1111
|
+
processArrays(schemaObject.additionalProperties);
|
|
1112
|
+
processSchemaArray(schemaObject.allOf);
|
|
1113
|
+
processSchemaArray(schemaObject.anyOf);
|
|
1114
|
+
processSchemaArray(schemaObject.oneOf);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Process arrays in the schema object.
|
|
1119
|
+
* @param schemaDictionary The schema object to process.
|
|
1120
|
+
*/
|
|
1121
|
+
function processSchemaDictionary(schemaDictionary) {
|
|
1122
|
+
if (core.Is.object(schemaDictionary)) {
|
|
1123
|
+
for (const item of Object.values(schemaDictionary)) {
|
|
1124
|
+
if (core.Is.object(item)) {
|
|
1125
|
+
processArrays(item);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Process arrays in the schema object.
|
|
1132
|
+
* @param schemaArray The schema object to process.
|
|
1133
|
+
*/
|
|
1134
|
+
function processSchemaArray(schemaArray) {
|
|
1135
|
+
if (core.Is.arrayValue(schemaArray)) {
|
|
1136
|
+
for (const item of schemaArray) {
|
|
1137
|
+
if (core.Is.object(item)) {
|
|
1138
|
+
processArrays(item);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Cleanup TypeScript markers from the type name.
|
|
1145
|
+
* @param typeName The definition string to clean up.
|
|
1146
|
+
* @returns The cleaned up definition string.
|
|
1147
|
+
*/
|
|
1148
|
+
function normaliseTypeName(typeName) {
|
|
1149
|
+
// Remove the partial markers
|
|
1150
|
+
let sTypeName = typeName.replace(/^Partial<(.*?)>/g, "$1");
|
|
1151
|
+
sTypeName = sTypeName.replace(/Partial%3CI(.*?)%3E/g, "$1");
|
|
1152
|
+
// Remove the omit markers
|
|
1153
|
+
sTypeName = sTypeName.replace(/^Omit<(.*?),.*>/g, "$1");
|
|
1154
|
+
sTypeName = sTypeName.replace(/Omit%3CI(.*?)%2C.*%3E/g, "$1");
|
|
1155
|
+
// Remove the pick markers
|
|
1156
|
+
sTypeName = sTypeName.replace(/^Pick<(.*?),.*>/g, "$1");
|
|
1157
|
+
sTypeName = sTypeName.replace(/Pick%3CI(.*?)%2C.*%3E/g, "$1");
|
|
1158
|
+
// Cleanup the generic markers
|
|
1159
|
+
sTypeName = sTypeName.replace(/</g, "%3C").replace(/>/g, "%3E");
|
|
1160
|
+
sTypeName = sTypeName.replace(/%3Cunknown%3E/g, "");
|
|
1161
|
+
return sTypeName;
|
|
1162
|
+
}
|
|
1101
1163
|
|
|
1102
1164
|
// Copyright 2024 IOTA Stiftung.
|
|
1103
1165
|
// SPDX-License-Identifier: Apache-2.0.
|
|
@@ -1117,7 +1179,7 @@ class CLI extends cliCore.CLIBase {
|
|
|
1117
1179
|
return this.execute({
|
|
1118
1180
|
title: "TWIN TypeScript To OpenAPI",
|
|
1119
1181
|
appName: "ts-to-openapi",
|
|
1120
|
-
version: "0.0.1-next.
|
|
1182
|
+
version: "0.0.1-next.28", // x-release-please-version
|
|
1121
1183
|
icon: "⚙️ ",
|
|
1122
1184
|
supportsEnvFiles: false,
|
|
1123
1185
|
overrideOutputWidth: options?.overrideOutputWidth
|
package/dist/esm/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { CLIDisplay, CLIUtils, CLIBase } from '@twin.org/cli-core';
|
|
4
4
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { I18n, GeneralError, Is, StringHelper, ObjectHelper } from '@twin.org/core';
|
|
5
|
+
import { I18n, GeneralError, Is, StringHelper, ObjectHelper, ArrayHelper } from '@twin.org/core';
|
|
6
6
|
import { HttpStatusCode, MimeTypes } from '@twin.org/web';
|
|
7
7
|
import { createGenerator } from 'ts-json-schema-generator';
|
|
8
8
|
|
|
@@ -656,11 +656,13 @@ async function finaliseOutput(usedCommonResponseTypes, schemas, openApi, securit
|
|
|
656
656
|
}
|
|
657
657
|
}
|
|
658
658
|
}
|
|
659
|
-
|
|
660
|
-
|
|
659
|
+
// Remove standard types that we don't want in the final output
|
|
660
|
+
const removeTypes = ["HttpStatusCode", "Uint8Array", "ArrayBuffer"];
|
|
661
|
+
for (const type of removeTypes) {
|
|
662
|
+
delete finalSchemas[type];
|
|
661
663
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
+
for (const type in finalSchemas) {
|
|
665
|
+
processArrays(finalSchemas[type]);
|
|
664
666
|
}
|
|
665
667
|
const schemaKeys = Object.keys(finalSchemas);
|
|
666
668
|
schemaKeys.sort();
|
|
@@ -693,12 +695,7 @@ async function finaliseOutput(usedCommonResponseTypes, schemas, openApi, securit
|
|
|
693
695
|
// Remove the array [] from the type names
|
|
694
696
|
// eslint-disable-next-line unicorn/better-regex
|
|
695
697
|
json = json.replace(/#\/components\/schemas\/(.*)\[\]/g, "#/components/schemas/ListOf$1");
|
|
696
|
-
|
|
697
|
-
json = json.replace(/Partial%3CI(.*?)%3E/g, "$1");
|
|
698
|
-
// Remove the omit markers
|
|
699
|
-
json = json.replace(/Omit%3CI(.*?)%2C.*%3E/g, "$1");
|
|
700
|
-
// Cleanup the generic markers
|
|
701
|
-
json = json.replace(/%3Cunknown%3E/g, "");
|
|
698
|
+
json = normaliseTypeName(json);
|
|
702
699
|
// Remove external references
|
|
703
700
|
for (const finalExternal in finalExternals) {
|
|
704
701
|
json = json.replace(new RegExp(`"#/components/schemas/${StringHelper.stripPrefix(finalExternal)}"`, "g"), `"${finalExternals[finalExternal]}"`);
|
|
@@ -852,12 +849,7 @@ async function generateSchemas(modelDirWildcards, types, outputWorkingDir) {
|
|
|
852
849
|
const schema = generator.createSchema("*");
|
|
853
850
|
if (schema.definitions) {
|
|
854
851
|
for (const def in schema.definitions) {
|
|
855
|
-
|
|
856
|
-
let defSub = def.replace(/^Partial<(.*?)>/g, "$1");
|
|
857
|
-
// Remove the omit markers
|
|
858
|
-
defSub = defSub.replace(/^Omit<(.*?),.*>/g, "$1");
|
|
859
|
-
// Cleanup the generic markers
|
|
860
|
-
defSub = defSub.replace(/</g, "%3C").replace(/>/g, "%3E");
|
|
852
|
+
const defSub = normaliseTypeName(def);
|
|
861
853
|
allSchemas[defSub] = schema.definitions[def];
|
|
862
854
|
}
|
|
863
855
|
}
|
|
@@ -899,10 +891,7 @@ function extractTypes(allSchemas, requiredTypes, referencedSchemas) {
|
|
|
899
891
|
function extractTypesFromSchema(allTypes, schema, output) {
|
|
900
892
|
const additionalTypes = [];
|
|
901
893
|
if (Is.stringValue(schema.$ref)) {
|
|
902
|
-
additionalTypes.push(schema.$ref
|
|
903
|
-
.replace("#/definitions/", "")
|
|
904
|
-
.replace(/^Partial%3C(.*?)%3E/g, "$1")
|
|
905
|
-
.replace(/^Omit%3C(.*?)%2C.*%3E/g, "$1"));
|
|
894
|
+
additionalTypes.push(normaliseTypeName(schema.$ref).replace("#/definitions/", ""));
|
|
906
895
|
}
|
|
907
896
|
else if (Is.object(schema.items)) {
|
|
908
897
|
if (Is.arrayValue(schema.items)) {
|
|
@@ -1095,6 +1084,79 @@ async function loadPackages(tsToOpenApiConfig, outputWorkingDir, typeRoots) {
|
|
|
1095
1084
|
}
|
|
1096
1085
|
return restRoutes;
|
|
1097
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Process arrays in the schema object.
|
|
1089
|
+
* @param schemaObject The schema object to process.
|
|
1090
|
+
*/
|
|
1091
|
+
function processArrays(schemaObject) {
|
|
1092
|
+
if (Is.object(schemaObject)) {
|
|
1093
|
+
// latest specs have singular items in `items` property
|
|
1094
|
+
// and multiple items in prefixItems, so update the schema accordingly
|
|
1095
|
+
// https://www.learnjsonschema.com/2020-12/applicator/items/
|
|
1096
|
+
// https://www.learnjsonschema.com/2020-12/applicator/prefixitems/
|
|
1097
|
+
const schemaItems = schemaObject.items;
|
|
1098
|
+
if (Is.array(schemaItems) || Is.object(schemaItems)) {
|
|
1099
|
+
schemaObject.prefixItems = ArrayHelper.fromObjectOrArray(schemaItems);
|
|
1100
|
+
delete schemaObject.items;
|
|
1101
|
+
}
|
|
1102
|
+
const additionalItems = schemaObject.additionalItems;
|
|
1103
|
+
if (Is.array(additionalItems) || Is.object(additionalItems)) {
|
|
1104
|
+
schemaObject.items = ArrayHelper.fromObjectOrArray(additionalItems)[0];
|
|
1105
|
+
delete schemaObject.additionalItems;
|
|
1106
|
+
}
|
|
1107
|
+
processSchemaDictionary(schemaObject.properties);
|
|
1108
|
+
processArrays(schemaObject.additionalProperties);
|
|
1109
|
+
processSchemaArray(schemaObject.allOf);
|
|
1110
|
+
processSchemaArray(schemaObject.anyOf);
|
|
1111
|
+
processSchemaArray(schemaObject.oneOf);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Process arrays in the schema object.
|
|
1116
|
+
* @param schemaDictionary The schema object to process.
|
|
1117
|
+
*/
|
|
1118
|
+
function processSchemaDictionary(schemaDictionary) {
|
|
1119
|
+
if (Is.object(schemaDictionary)) {
|
|
1120
|
+
for (const item of Object.values(schemaDictionary)) {
|
|
1121
|
+
if (Is.object(item)) {
|
|
1122
|
+
processArrays(item);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Process arrays in the schema object.
|
|
1129
|
+
* @param schemaArray The schema object to process.
|
|
1130
|
+
*/
|
|
1131
|
+
function processSchemaArray(schemaArray) {
|
|
1132
|
+
if (Is.arrayValue(schemaArray)) {
|
|
1133
|
+
for (const item of schemaArray) {
|
|
1134
|
+
if (Is.object(item)) {
|
|
1135
|
+
processArrays(item);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Cleanup TypeScript markers from the type name.
|
|
1142
|
+
* @param typeName The definition string to clean up.
|
|
1143
|
+
* @returns The cleaned up definition string.
|
|
1144
|
+
*/
|
|
1145
|
+
function normaliseTypeName(typeName) {
|
|
1146
|
+
// Remove the partial markers
|
|
1147
|
+
let sTypeName = typeName.replace(/^Partial<(.*?)>/g, "$1");
|
|
1148
|
+
sTypeName = sTypeName.replace(/Partial%3CI(.*?)%3E/g, "$1");
|
|
1149
|
+
// Remove the omit markers
|
|
1150
|
+
sTypeName = sTypeName.replace(/^Omit<(.*?),.*>/g, "$1");
|
|
1151
|
+
sTypeName = sTypeName.replace(/Omit%3CI(.*?)%2C.*%3E/g, "$1");
|
|
1152
|
+
// Remove the pick markers
|
|
1153
|
+
sTypeName = sTypeName.replace(/^Pick<(.*?),.*>/g, "$1");
|
|
1154
|
+
sTypeName = sTypeName.replace(/Pick%3CI(.*?)%2C.*%3E/g, "$1");
|
|
1155
|
+
// Cleanup the generic markers
|
|
1156
|
+
sTypeName = sTypeName.replace(/</g, "%3C").replace(/>/g, "%3E");
|
|
1157
|
+
sTypeName = sTypeName.replace(/%3Cunknown%3E/g, "");
|
|
1158
|
+
return sTypeName;
|
|
1159
|
+
}
|
|
1098
1160
|
|
|
1099
1161
|
// Copyright 2024 IOTA Stiftung.
|
|
1100
1162
|
// SPDX-License-Identifier: Apache-2.0.
|
|
@@ -1114,7 +1176,7 @@ class CLI extends CLIBase {
|
|
|
1114
1176
|
return this.execute({
|
|
1115
1177
|
title: "TWIN TypeScript To OpenAPI",
|
|
1116
1178
|
appName: "ts-to-openapi",
|
|
1117
|
-
version: "0.0.1-next.
|
|
1179
|
+
version: "0.0.1-next.28", // x-release-please-version
|
|
1118
1180
|
icon: "⚙️ ",
|
|
1119
1181
|
supportsEnvFiles: false,
|
|
1120
1182
|
overrideOutputWidth: options?.overrideOutputWidth
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @twin.org/ts-to-openapi - Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.1-next.28](https://github.com/twinfoundation/tools/compare/ts-to-openapi-v0.0.1-next.27...ts-to-openapi-v0.0.1-next.28) (2025-06-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* improve schema type name normalisation ([1a18b26](https://github.com/twinfoundation/tools/commit/1a18b267d87e9179bda01b396b256c450ae2889e))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/nameof bumped from 0.0.1-next.27 to 0.0.1-next.28
|
|
16
|
+
* devDependencies
|
|
17
|
+
* @twin.org/merge-locales bumped from 0.0.1-next.27 to 0.0.1-next.28
|
|
18
|
+
* @twin.org/nameof-transformer bumped from 0.0.1-next.27 to 0.0.1-next.28
|
|
19
|
+
* @twin.org/nameof-vitest-plugin bumped from 0.0.1-next.27 to 0.0.1-next.28
|
|
20
|
+
|
|
21
|
+
## [0.0.1-next.27](https://github.com/twinfoundation/tools/compare/ts-to-openapi-v0.0.1-next.26...ts-to-openapi-v0.0.1-next.27) (2025-06-17)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* add latest json schema features ([494293f](https://github.com/twinfoundation/tools/commit/494293f4252b9c7d4a20790ec157fc9d8c96c3d2))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Dependencies
|
|
30
|
+
|
|
31
|
+
* The following workspace dependencies were updated
|
|
32
|
+
* dependencies
|
|
33
|
+
* @twin.org/nameof bumped from 0.0.1-next.26 to 0.0.1-next.27
|
|
34
|
+
* devDependencies
|
|
35
|
+
* @twin.org/merge-locales bumped from 0.0.1-next.26 to 0.0.1-next.27
|
|
36
|
+
* @twin.org/nameof-transformer bumped from 0.0.1-next.26 to 0.0.1-next.27
|
|
37
|
+
* @twin.org/nameof-vitest-plugin bumped from 0.0.1-next.26 to 0.0.1-next.27
|
|
38
|
+
|
|
3
39
|
## [0.0.1-next.26](https://github.com/twinfoundation/tools/compare/ts-to-openapi-v0.0.1-next.25...ts-to-openapi-v0.0.1-next.26) (2025-06-11)
|
|
4
40
|
|
|
5
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/ts-to-openapi",
|
|
3
|
-
"version": "0.0.1-next.
|
|
3
|
+
"version": "0.0.1-next.28",
|
|
4
4
|
"description": "Tool to convert TypeScript REST route definitions to OpenAPI Specifications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@twin.org/api-models": "next",
|
|
18
18
|
"@twin.org/cli-core": "next",
|
|
19
19
|
"@twin.org/core": "next",
|
|
20
|
-
"@twin.org/nameof": "0.0.1-next.
|
|
20
|
+
"@twin.org/nameof": "0.0.1-next.28",
|
|
21
21
|
"@twin.org/web": "next",
|
|
22
22
|
"ajv": "8.17.1",
|
|
23
23
|
"commander": "14.0.0",
|