@go-to-k/cdkd 0.51.2 → 0.51.4
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/cli.js +248 -20
- package/dist/cli.js.map +2 -2
- package/dist/go-to-k-cdkd-0.51.4.tgz +0 -0
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.51.2.tgz +0 -0
package/dist/cli.js
CHANGED
|
@@ -21733,10 +21733,21 @@ var EC2Provider = class {
|
|
|
21733
21733
|
* `AllocationId`, `ConnectivityType`, `PrivateIpAddress`.
|
|
21734
21734
|
* - **AWS::EC2::RouteTable**: `DescribeRouteTables` for `VpcId`.
|
|
21735
21735
|
* - **AWS::EC2::SecurityGroup**: `DescribeSecurityGroups` for
|
|
21736
|
-
* `GroupName`, `GroupDescription`, `VpcId
|
|
21737
|
-
*
|
|
21738
|
-
*
|
|
21739
|
-
*
|
|
21736
|
+
* `GroupName`, `GroupDescription`, `VpcId`, plus `SecurityGroupIngress`
|
|
21737
|
+
* and `SecurityGroupEgress` reverse-mapped from AWS's normalized
|
|
21738
|
+
* `IpPermissions[]` / `IpPermissionsEgress[]` form. Each AWS
|
|
21739
|
+
* `IpPermission` is flattened into one CFn rule per `IpRanges` /
|
|
21740
|
+
* `Ipv6Ranges` / `UserIdGroupPairs` / `PrefixListIds` entry; field
|
|
21741
|
+
* names follow CFn direction conventions (`Source*` for ingress,
|
|
21742
|
+
* `Destination*` for egress). When state templates ingress/egress
|
|
21743
|
+
* rules, AWS's response is reordered to match state's positional
|
|
21744
|
+
* order via `reconcileSgRules` so the comparator's array compare
|
|
21745
|
+
* doesn't fire false drift on AWS's normalized ordering. The
|
|
21746
|
+
* AWS-auto-attached "allow-all 0.0.0.0/0" egress rule is filtered
|
|
21747
|
+
* out of `SecurityGroupEgress` when state did not template egress
|
|
21748
|
+
* (the auto-default is invisible to drift). Both arrays are
|
|
21749
|
+
* always emitted (even as `[]`) so the v3 `observedProperties`
|
|
21750
|
+
* baseline catches console-side rule ADDs to a templated SG.
|
|
21740
21751
|
* - **AWS::EC2::Instance**: `DescribeInstances` for `ImageId`,
|
|
21741
21752
|
* `InstanceType`, `KeyName`, `SubnetId`. SecurityGroupIds /
|
|
21742
21753
|
* BlockDeviceMappings shape-match is out of scope for v1.
|
|
@@ -21761,7 +21772,7 @@ var EC2Provider = class {
|
|
|
21761
21772
|
* Returns `undefined` when the resource is gone (any `*NotFound` /
|
|
21762
21773
|
* `Invalid*` error from the EC2 SDK matches `isNotFoundError`).
|
|
21763
21774
|
*/
|
|
21764
|
-
async readCurrentState(physicalId, logicalId, resourceType) {
|
|
21775
|
+
async readCurrentState(physicalId, logicalId, resourceType, properties) {
|
|
21765
21776
|
try {
|
|
21766
21777
|
switch (resourceType) {
|
|
21767
21778
|
case "AWS::EC2::VPC":
|
|
@@ -21775,7 +21786,7 @@ var EC2Provider = class {
|
|
|
21775
21786
|
case "AWS::EC2::RouteTable":
|
|
21776
21787
|
return await this.readRouteTableCurrentState(physicalId);
|
|
21777
21788
|
case "AWS::EC2::SecurityGroup":
|
|
21778
|
-
return await this.readSecurityGroupCurrentState(physicalId);
|
|
21789
|
+
return await this.readSecurityGroupCurrentState(physicalId, properties);
|
|
21779
21790
|
case "AWS::EC2::Instance":
|
|
21780
21791
|
return await this.readInstanceCurrentState(physicalId);
|
|
21781
21792
|
case "AWS::EC2::NetworkAcl":
|
|
@@ -21884,7 +21895,7 @@ var EC2Provider = class {
|
|
|
21884
21895
|
result["VpcId"] = rt.VpcId;
|
|
21885
21896
|
return result;
|
|
21886
21897
|
}
|
|
21887
|
-
async readSecurityGroupCurrentState(physicalId) {
|
|
21898
|
+
async readSecurityGroupCurrentState(physicalId, properties) {
|
|
21888
21899
|
const resp = await this.ec2Client.send(
|
|
21889
21900
|
new DescribeSecurityGroupsCommand2({ GroupIds: [physicalId] })
|
|
21890
21901
|
);
|
|
@@ -21898,6 +21909,15 @@ var EC2Provider = class {
|
|
|
21898
21909
|
result["GroupDescription"] = sg.Description;
|
|
21899
21910
|
if (sg.VpcId !== void 0)
|
|
21900
21911
|
result["VpcId"] = sg.VpcId;
|
|
21912
|
+
const stateIngress = Array.isArray(properties?.["SecurityGroupIngress"]) ? properties["SecurityGroupIngress"] : void 0;
|
|
21913
|
+
const ingressRules = flattenIpPermissions(sg.IpPermissions ?? [], "ingress");
|
|
21914
|
+
result["SecurityGroupIngress"] = reconcileSgRules(ingressRules, stateIngress, "ingress");
|
|
21915
|
+
const stateEgress = Array.isArray(properties?.["SecurityGroupEgress"]) ? properties["SecurityGroupEgress"] : void 0;
|
|
21916
|
+
let egressRules = flattenIpPermissions(sg.IpPermissionsEgress ?? [], "egress");
|
|
21917
|
+
if (stateEgress === void 0) {
|
|
21918
|
+
egressRules = egressRules.filter((r) => !isDefaultEgressRule(r));
|
|
21919
|
+
}
|
|
21920
|
+
result["SecurityGroupEgress"] = reconcileSgRules(egressRules, stateEgress, "egress");
|
|
21901
21921
|
return result;
|
|
21902
21922
|
}
|
|
21903
21923
|
async readInstanceCurrentState(physicalId) {
|
|
@@ -21967,6 +21987,115 @@ var EC2Provider = class {
|
|
|
21967
21987
|
}
|
|
21968
21988
|
}
|
|
21969
21989
|
};
|
|
21990
|
+
function flattenIpPermissions(perms, direction) {
|
|
21991
|
+
const out = [];
|
|
21992
|
+
for (const p of perms) {
|
|
21993
|
+
const base = {};
|
|
21994
|
+
if (p.IpProtocol !== void 0)
|
|
21995
|
+
base["IpProtocol"] = p.IpProtocol;
|
|
21996
|
+
if (p.FromPort !== void 0)
|
|
21997
|
+
base["FromPort"] = p.FromPort;
|
|
21998
|
+
if (p.ToPort !== void 0)
|
|
21999
|
+
base["ToPort"] = p.ToPort;
|
|
22000
|
+
for (const ip of p.IpRanges ?? []) {
|
|
22001
|
+
const rule = { ...base };
|
|
22002
|
+
if (ip.CidrIp !== void 0)
|
|
22003
|
+
rule["CidrIp"] = ip.CidrIp;
|
|
22004
|
+
if (ip.Description !== void 0)
|
|
22005
|
+
rule["Description"] = ip.Description;
|
|
22006
|
+
out.push(rule);
|
|
22007
|
+
}
|
|
22008
|
+
for (const ipv6 of p.Ipv6Ranges ?? []) {
|
|
22009
|
+
const rule = { ...base };
|
|
22010
|
+
if (ipv6.CidrIpv6 !== void 0)
|
|
22011
|
+
rule["CidrIpv6"] = ipv6.CidrIpv6;
|
|
22012
|
+
if (ipv6.Description !== void 0)
|
|
22013
|
+
rule["Description"] = ipv6.Description;
|
|
22014
|
+
out.push(rule);
|
|
22015
|
+
}
|
|
22016
|
+
for (const grp of p.UserIdGroupPairs ?? []) {
|
|
22017
|
+
const rule = { ...base };
|
|
22018
|
+
if (direction === "ingress") {
|
|
22019
|
+
if (grp.GroupId !== void 0)
|
|
22020
|
+
rule["SourceSecurityGroupId"] = grp.GroupId;
|
|
22021
|
+
if (grp.UserId !== void 0)
|
|
22022
|
+
rule["SourceSecurityGroupOwnerId"] = grp.UserId;
|
|
22023
|
+
} else {
|
|
22024
|
+
if (grp.GroupId !== void 0)
|
|
22025
|
+
rule["DestinationSecurityGroupId"] = grp.GroupId;
|
|
22026
|
+
}
|
|
22027
|
+
if (grp.Description !== void 0)
|
|
22028
|
+
rule["Description"] = grp.Description;
|
|
22029
|
+
out.push(rule);
|
|
22030
|
+
}
|
|
22031
|
+
for (const pl of p.PrefixListIds ?? []) {
|
|
22032
|
+
const rule = { ...base };
|
|
22033
|
+
if (direction === "ingress") {
|
|
22034
|
+
if (pl.PrefixListId !== void 0)
|
|
22035
|
+
rule["SourcePrefixListId"] = pl.PrefixListId;
|
|
22036
|
+
} else {
|
|
22037
|
+
if (pl.PrefixListId !== void 0)
|
|
22038
|
+
rule["DestinationPrefixListId"] = pl.PrefixListId;
|
|
22039
|
+
}
|
|
22040
|
+
if (pl.Description !== void 0)
|
|
22041
|
+
rule["Description"] = pl.Description;
|
|
22042
|
+
out.push(rule);
|
|
22043
|
+
}
|
|
22044
|
+
if ((p.IpRanges?.length ?? 0) === 0 && (p.Ipv6Ranges?.length ?? 0) === 0 && (p.UserIdGroupPairs?.length ?? 0) === 0 && (p.PrefixListIds?.length ?? 0) === 0) {
|
|
22045
|
+
out.push({ ...base });
|
|
22046
|
+
}
|
|
22047
|
+
}
|
|
22048
|
+
return out;
|
|
22049
|
+
}
|
|
22050
|
+
function sgRuleKey(rule, direction) {
|
|
22051
|
+
const peerKey = direction === "egress" ? rule["DestinationSecurityGroupId"] : rule["SourceSecurityGroupId"];
|
|
22052
|
+
const prefixKey = direction === "egress" ? rule["DestinationPrefixListId"] : rule["SourcePrefixListId"];
|
|
22053
|
+
const peerOwner = direction === "ingress" ? rule["SourceSecurityGroupOwnerId"] : void 0;
|
|
22054
|
+
return JSON.stringify({
|
|
22055
|
+
p: rule["IpProtocol"] ?? "-1",
|
|
22056
|
+
f: rule["FromPort"] ?? null,
|
|
22057
|
+
t: rule["ToPort"] ?? null,
|
|
22058
|
+
c4: rule["CidrIp"] ?? null,
|
|
22059
|
+
c6: rule["CidrIpv6"] ?? null,
|
|
22060
|
+
peer: peerKey ?? null,
|
|
22061
|
+
peerOwner: peerOwner ?? null,
|
|
22062
|
+
pl: prefixKey ?? null,
|
|
22063
|
+
d: rule["Description"] ?? null
|
|
22064
|
+
});
|
|
22065
|
+
}
|
|
22066
|
+
function reconcileSgRules(awsRules, stateRules, direction) {
|
|
22067
|
+
if (!stateRules || stateRules.length === 0)
|
|
22068
|
+
return awsRules;
|
|
22069
|
+
const remaining = [...awsRules];
|
|
22070
|
+
const reordered = [];
|
|
22071
|
+
for (const sr of stateRules) {
|
|
22072
|
+
const key = sgRuleKey(sr, direction);
|
|
22073
|
+
const idx = remaining.findIndex((ar) => sgRuleKey(ar, direction) === key);
|
|
22074
|
+
if (idx >= 0) {
|
|
22075
|
+
reordered.push(remaining.splice(idx, 1)[0]);
|
|
22076
|
+
}
|
|
22077
|
+
}
|
|
22078
|
+
return [...reordered, ...remaining];
|
|
22079
|
+
}
|
|
22080
|
+
function isDefaultEgressRule(rule) {
|
|
22081
|
+
if (rule["IpProtocol"] !== "-1")
|
|
22082
|
+
return false;
|
|
22083
|
+
if (rule["CidrIp"] !== "0.0.0.0/0")
|
|
22084
|
+
return false;
|
|
22085
|
+
if (rule["CidrIpv6"] !== void 0)
|
|
22086
|
+
return false;
|
|
22087
|
+
if (rule["DestinationSecurityGroupId"] !== void 0)
|
|
22088
|
+
return false;
|
|
22089
|
+
if (rule["DestinationPrefixListId"] !== void 0)
|
|
22090
|
+
return false;
|
|
22091
|
+
if (rule["Description"] !== void 0)
|
|
22092
|
+
return false;
|
|
22093
|
+
if (rule["FromPort"] !== void 0 && rule["FromPort"] !== -1)
|
|
22094
|
+
return false;
|
|
22095
|
+
if (rule["ToPort"] !== void 0 && rule["ToPort"] !== -1)
|
|
22096
|
+
return false;
|
|
22097
|
+
return true;
|
|
22098
|
+
}
|
|
21970
22099
|
|
|
21971
22100
|
// src/provisioning/providers/apigateway-provider.ts
|
|
21972
22101
|
import {
|
|
@@ -35218,20 +35347,26 @@ var FirehoseProvider = class {
|
|
|
35218
35347
|
* `KinesisStreamSourceConfiguration` parent fields when present (the
|
|
35219
35348
|
* `DescribeDeliveryStream` response splits source under `Source.KinesisStreamSourceDescription`).
|
|
35220
35349
|
*
|
|
35221
|
-
* Destination configurations
|
|
35222
|
-
*
|
|
35223
|
-
* not
|
|
35224
|
-
*
|
|
35225
|
-
*
|
|
35226
|
-
*
|
|
35227
|
-
*
|
|
35228
|
-
*
|
|
35229
|
-
* `
|
|
35350
|
+
* **Destination configurations**: partial coverage. AWS returns destination
|
|
35351
|
+
* config under `Destinations[0].*DestinationDescription` (note:
|
|
35352
|
+
* `Description`, not `Configuration`). For S3 / ExtendedS3 destinations
|
|
35353
|
+
* the top-level fields with a clean reverse-mapping are surfaced —
|
|
35354
|
+
* `BucketARN`, `RoleARN`, `Prefix`, `ErrorOutputPrefix`, `BufferingHints`,
|
|
35355
|
+
* `CompressionFormat`, plus `S3BackupMode` for Extended. Inner nested
|
|
35356
|
+
* fields (`EncryptionConfiguration`, `CloudWatchLoggingOptions`,
|
|
35357
|
+
* `ProcessingConfiguration`, `DataFormatConversionConfiguration`,
|
|
35358
|
+
* `DynamicPartitioningConfiguration`, `S3BackupConfiguration`) are not
|
|
35359
|
+
* re-shaped — AWS auto-defaults / extra-metadata / write-only redaction
|
|
35360
|
+
* (`Password`) make the round-trip unsafe; they're declared via
|
|
35361
|
+
* `getDriftUnknownPaths()` so the comparator skips them instead of firing
|
|
35362
|
+
* false drift. Non-S3 destination types
|
|
35363
|
+
* (`Redshift`/`Elasticsearch`/`Amazonopensearchservice`/`Splunk`/`HttpEndpoint`/`AmazonOpenSearchServerless`)
|
|
35364
|
+
* stay drift-unknown for v1 — same shape-divergence problem at scale.
|
|
35365
|
+
* `DeliveryStreamEncryptionConfigurationInput` also drift-unknown.
|
|
35230
35366
|
*
|
|
35231
35367
|
* Tags are surfaced via a follow-up `ListTagsForDeliveryStream` call
|
|
35232
|
-
* with `aws:*` filtered out and
|
|
35233
|
-
*
|
|
35234
|
-
* decision deferred).
|
|
35368
|
+
* with `aws:*` filtered out and always emitted as `[]` placeholder when
|
|
35369
|
+
* no user tags remain.
|
|
35235
35370
|
*
|
|
35236
35371
|
* Returns `undefined` when the stream is gone (`ResourceNotFoundException`).
|
|
35237
35372
|
*/
|
|
@@ -35267,6 +35402,60 @@ var FirehoseProvider = class {
|
|
|
35267
35402
|
result["KinesisStreamSourceConfiguration"] = srcOut;
|
|
35268
35403
|
}
|
|
35269
35404
|
}
|
|
35405
|
+
const dest = desc.Destinations?.[0];
|
|
35406
|
+
if (dest?.ExtendedS3DestinationDescription) {
|
|
35407
|
+
const ext = dest.ExtendedS3DestinationDescription;
|
|
35408
|
+
const out = {};
|
|
35409
|
+
if (ext.BucketARN !== void 0)
|
|
35410
|
+
out["BucketARN"] = ext.BucketARN;
|
|
35411
|
+
if (ext.RoleARN !== void 0)
|
|
35412
|
+
out["RoleARN"] = ext.RoleARN;
|
|
35413
|
+
if (ext.Prefix !== void 0)
|
|
35414
|
+
out["Prefix"] = ext.Prefix;
|
|
35415
|
+
if (ext.ErrorOutputPrefix !== void 0)
|
|
35416
|
+
out["ErrorOutputPrefix"] = ext.ErrorOutputPrefix;
|
|
35417
|
+
if (ext.CompressionFormat !== void 0)
|
|
35418
|
+
out["CompressionFormat"] = ext.CompressionFormat;
|
|
35419
|
+
if (ext.BufferingHints) {
|
|
35420
|
+
const hints = {};
|
|
35421
|
+
if (ext.BufferingHints.SizeInMBs !== void 0)
|
|
35422
|
+
hints["SizeInMBs"] = ext.BufferingHints.SizeInMBs;
|
|
35423
|
+
if (ext.BufferingHints.IntervalInSeconds !== void 0)
|
|
35424
|
+
hints["IntervalInSeconds"] = ext.BufferingHints.IntervalInSeconds;
|
|
35425
|
+
if (Object.keys(hints).length > 0)
|
|
35426
|
+
out["BufferingHints"] = hints;
|
|
35427
|
+
}
|
|
35428
|
+
if (ext.S3BackupMode !== void 0)
|
|
35429
|
+
out["S3BackupMode"] = ext.S3BackupMode;
|
|
35430
|
+
if (Object.keys(out).length > 0) {
|
|
35431
|
+
result["ExtendedS3DestinationConfiguration"] = out;
|
|
35432
|
+
}
|
|
35433
|
+
} else if (dest?.S3DestinationDescription) {
|
|
35434
|
+
const s3 = dest.S3DestinationDescription;
|
|
35435
|
+
const out = {};
|
|
35436
|
+
if (s3.BucketARN !== void 0)
|
|
35437
|
+
out["BucketARN"] = s3.BucketARN;
|
|
35438
|
+
if (s3.RoleARN !== void 0)
|
|
35439
|
+
out["RoleARN"] = s3.RoleARN;
|
|
35440
|
+
if (s3.Prefix !== void 0)
|
|
35441
|
+
out["Prefix"] = s3.Prefix;
|
|
35442
|
+
if (s3.ErrorOutputPrefix !== void 0)
|
|
35443
|
+
out["ErrorOutputPrefix"] = s3.ErrorOutputPrefix;
|
|
35444
|
+
if (s3.CompressionFormat !== void 0)
|
|
35445
|
+
out["CompressionFormat"] = s3.CompressionFormat;
|
|
35446
|
+
if (s3.BufferingHints) {
|
|
35447
|
+
const hints = {};
|
|
35448
|
+
if (s3.BufferingHints.SizeInMBs !== void 0)
|
|
35449
|
+
hints["SizeInMBs"] = s3.BufferingHints.SizeInMBs;
|
|
35450
|
+
if (s3.BufferingHints.IntervalInSeconds !== void 0)
|
|
35451
|
+
hints["IntervalInSeconds"] = s3.BufferingHints.IntervalInSeconds;
|
|
35452
|
+
if (Object.keys(hints).length > 0)
|
|
35453
|
+
out["BufferingHints"] = hints;
|
|
35454
|
+
}
|
|
35455
|
+
if (Object.keys(out).length > 0) {
|
|
35456
|
+
result["S3DestinationConfiguration"] = out;
|
|
35457
|
+
}
|
|
35458
|
+
}
|
|
35270
35459
|
try {
|
|
35271
35460
|
const tagsResp = await this.getClient().send(
|
|
35272
35461
|
new ListTagsForDeliveryStreamCommand({ DeliveryStreamName: physicalId })
|
|
@@ -35282,6 +35471,45 @@ var FirehoseProvider = class {
|
|
|
35282
35471
|
}
|
|
35283
35472
|
return result;
|
|
35284
35473
|
}
|
|
35474
|
+
/**
|
|
35475
|
+
* Drift-unknown paths for `AWS::KinesisFirehose::DeliveryStream`.
|
|
35476
|
+
*
|
|
35477
|
+
* The drift comparator skips these state property paths so they never
|
|
35478
|
+
* fire false-positive drift on every run. See the `readCurrentState`
|
|
35479
|
+
* docstring for the full rationale per category.
|
|
35480
|
+
*
|
|
35481
|
+
* Categories:
|
|
35482
|
+
* - Inner nested fields under S3 / ExtendedS3 destinations: shape
|
|
35483
|
+
* divergence between `Configuration` (CFn input) and `Description`
|
|
35484
|
+
* (AWS read), AWS auto-defaults, write-only fields.
|
|
35485
|
+
* - Non-S3 destination types: same shape-divergence problem at scale,
|
|
35486
|
+
* deferred to a follow-up.
|
|
35487
|
+
* - `DeliveryStreamEncryptionConfigurationInput`: input-only shape
|
|
35488
|
+
* (`KeyARN` + `KeyType`) vs. read-side `DeliveryStreamEncryptionConfiguration`
|
|
35489
|
+
* (extra status / failure fields); not yet round-tripped.
|
|
35490
|
+
*/
|
|
35491
|
+
getDriftUnknownPaths() {
|
|
35492
|
+
return [
|
|
35493
|
+
// S3 / ExtendedS3 nested fields with shape divergence
|
|
35494
|
+
"S3DestinationConfiguration.EncryptionConfiguration",
|
|
35495
|
+
"S3DestinationConfiguration.CloudWatchLoggingOptions",
|
|
35496
|
+
"ExtendedS3DestinationConfiguration.EncryptionConfiguration",
|
|
35497
|
+
"ExtendedS3DestinationConfiguration.CloudWatchLoggingOptions",
|
|
35498
|
+
"ExtendedS3DestinationConfiguration.ProcessingConfiguration",
|
|
35499
|
+
"ExtendedS3DestinationConfiguration.DataFormatConversionConfiguration",
|
|
35500
|
+
"ExtendedS3DestinationConfiguration.DynamicPartitioningConfiguration",
|
|
35501
|
+
"ExtendedS3DestinationConfiguration.S3BackupConfiguration",
|
|
35502
|
+
// Non-S3 destinations (drift-unknown for v1)
|
|
35503
|
+
"RedshiftDestinationConfiguration",
|
|
35504
|
+
"ElasticsearchDestinationConfiguration",
|
|
35505
|
+
"AmazonopensearchserviceDestinationConfiguration",
|
|
35506
|
+
"SplunkDestinationConfiguration",
|
|
35507
|
+
"HttpEndpointDestinationConfiguration",
|
|
35508
|
+
"AmazonOpenSearchServerlessDestinationConfiguration",
|
|
35509
|
+
// Encryption input shape (deferred)
|
|
35510
|
+
"DeliveryStreamEncryptionConfigurationInput"
|
|
35511
|
+
];
|
|
35512
|
+
}
|
|
35285
35513
|
async import(input) {
|
|
35286
35514
|
const explicit = resolveExplicitPhysicalId(input, "DeliveryStreamName");
|
|
35287
35515
|
if (explicit) {
|
|
@@ -44482,7 +44710,7 @@ function reorderArgs(argv) {
|
|
|
44482
44710
|
}
|
|
44483
44711
|
async function main() {
|
|
44484
44712
|
const program = new Command14();
|
|
44485
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.51.
|
|
44713
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.51.4");
|
|
44486
44714
|
program.addCommand(createBootstrapCommand());
|
|
44487
44715
|
program.addCommand(createSynthCommand());
|
|
44488
44716
|
program.addCommand(createListCommand());
|