@devramps/cli 0.1.22 → 0.1.24
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/index.js +120 -78
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1556,25 +1556,36 @@ function addOidcProviderResource(template, conditional = true, oidcProviderUrl)
|
|
|
1556
1556
|
}
|
|
1557
1557
|
};
|
|
1558
1558
|
}
|
|
1559
|
-
function buildOidcTrustPolicy(accountId, subject, oidcProviderUrl) {
|
|
1559
|
+
function buildOidcTrustPolicy(accountId, subject, oidcProviderUrl, additionalTrustedAccounts, skipOidc) {
|
|
1560
1560
|
const providerUrl = oidcProviderUrl || OIDC_PROVIDER_URL;
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
[`${providerUrl}:aud`]: "sts.amazonaws.com"
|
|
1574
|
-
}
|
|
1561
|
+
const statements = [];
|
|
1562
|
+
if (!skipOidc) {
|
|
1563
|
+
statements.push({
|
|
1564
|
+
Effect: "Allow",
|
|
1565
|
+
Principal: {
|
|
1566
|
+
Federated: `arn:aws:iam::${accountId}:oidc-provider/${providerUrl}`
|
|
1567
|
+
},
|
|
1568
|
+
Action: "sts:AssumeRoleWithWebIdentity",
|
|
1569
|
+
Condition: {
|
|
1570
|
+
StringEquals: {
|
|
1571
|
+
[`${providerUrl}:sub`]: subject,
|
|
1572
|
+
[`${providerUrl}:aud`]: "sts.amazonaws.com"
|
|
1575
1573
|
}
|
|
1576
1574
|
}
|
|
1577
|
-
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
if (additionalTrustedAccounts && additionalTrustedAccounts.length > 0) {
|
|
1578
|
+
statements.push({
|
|
1579
|
+
Effect: "Allow",
|
|
1580
|
+
Principal: {
|
|
1581
|
+
AWS: additionalTrustedAccounts.map((id) => `arn:aws:iam::${id}:root`)
|
|
1582
|
+
},
|
|
1583
|
+
Action: "sts:AssumeRole"
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
return {
|
|
1587
|
+
Version: "2012-10-17",
|
|
1588
|
+
Statement: statements
|
|
1578
1589
|
};
|
|
1579
1590
|
}
|
|
1580
1591
|
function createIamRoleResource(roleName, trustPolicy, policies, additionalTags = []) {
|
|
@@ -1938,7 +1949,7 @@ function createTerraformStateBucketPolicy(bucketName, cicdAccountId, allowedAcco
|
|
|
1938
1949
|
|
|
1939
1950
|
// src/templates/org-stack.ts
|
|
1940
1951
|
function generateOrgStackTemplate(options) {
|
|
1941
|
-
const { orgSlug, cicdAccountId, targetAccountIds, oidcProviderUrl } = options;
|
|
1952
|
+
const { orgSlug, cicdAccountId, targetAccountIds, oidcProviderUrl, additionalTrustedAccounts, skipOidc } = options;
|
|
1942
1953
|
const template = createBaseTemplate(`DevRamps Org Stack for ${orgSlug}`);
|
|
1943
1954
|
const kmsKeyPolicy = buildKmsKeyPolicy(cicdAccountId, targetAccountIds);
|
|
1944
1955
|
template.Resources.DevRampsKMSKey = createKmsKeyResource(
|
|
@@ -1968,7 +1979,7 @@ function generateOrgStackTemplate(options) {
|
|
|
1968
1979
|
PolicyDocument: bucketPolicy
|
|
1969
1980
|
}
|
|
1970
1981
|
};
|
|
1971
|
-
const trustPolicy = buildOidcTrustPolicy(cicdAccountId, `org:${orgSlug}/cicd`, oidcProviderUrl);
|
|
1982
|
+
const trustPolicy = buildOidcTrustPolicy(cicdAccountId, `org:${orgSlug}/cicd`, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
1972
1983
|
const orgRolePolicies = buildOrgRolePolicies(orgSlug);
|
|
1973
1984
|
template.Resources.DevRampsCICDDeploymentRole = createIamRoleResource(
|
|
1974
1985
|
getOrgRoleName(),
|
|
@@ -2433,13 +2444,15 @@ function generateStageStackTemplate(options) {
|
|
|
2433
2444
|
additionalPolicies,
|
|
2434
2445
|
dockerArtifacts,
|
|
2435
2446
|
bundleArtifacts,
|
|
2436
|
-
oidcProviderUrl
|
|
2447
|
+
oidcProviderUrl,
|
|
2448
|
+
additionalTrustedAccounts,
|
|
2449
|
+
skipOidc
|
|
2437
2450
|
} = options;
|
|
2438
2451
|
const template = createBaseTemplate(
|
|
2439
2452
|
`DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
|
|
2440
2453
|
);
|
|
2441
2454
|
const roleName = generateStageRoleName(pipelineSlug, stageName);
|
|
2442
|
-
const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, oidcProviderUrl);
|
|
2455
|
+
const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
2443
2456
|
const policies = buildStagePolicies(steps, additionalPolicies);
|
|
2444
2457
|
template.Resources.StageDeploymentRole = createIamRoleResource(
|
|
2445
2458
|
roleName,
|
|
@@ -2483,8 +2496,6 @@ function generateStageStackTemplate(options) {
|
|
|
2483
2496
|
);
|
|
2484
2497
|
s3Outputs[artifact.name] = { resourceId };
|
|
2485
2498
|
}
|
|
2486
|
-
const providerUrl = oidcProviderUrl || OIDC_PROVIDER_URL;
|
|
2487
|
-
const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${providerUrl}`;
|
|
2488
2499
|
template.Outputs = {
|
|
2489
2500
|
StageRoleArn: {
|
|
2490
2501
|
Description: "ARN of the stage deployment role",
|
|
@@ -2495,10 +2506,6 @@ function generateStageStackTemplate(options) {
|
|
|
2495
2506
|
Description: "Name of the stage deployment role",
|
|
2496
2507
|
Value: { Ref: "StageDeploymentRole" }
|
|
2497
2508
|
},
|
|
2498
|
-
OIDCProviderArn: {
|
|
2499
|
-
Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
|
|
2500
|
-
Value: oidcProviderArn
|
|
2501
|
-
},
|
|
2502
2509
|
PipelineSlug: {
|
|
2503
2510
|
Description: "Pipeline slug",
|
|
2504
2511
|
Value: pipelineSlug
|
|
@@ -2508,6 +2515,13 @@ function generateStageStackTemplate(options) {
|
|
|
2508
2515
|
Value: stageName
|
|
2509
2516
|
}
|
|
2510
2517
|
};
|
|
2518
|
+
if (!skipOidc) {
|
|
2519
|
+
const providerUrl = oidcProviderUrl || OIDC_PROVIDER_URL;
|
|
2520
|
+
template.Outputs.OIDCProviderArn = {
|
|
2521
|
+
Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
|
|
2522
|
+
Value: `arn:aws:iam::${accountId}:oidc-provider/${providerUrl}`
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2511
2525
|
for (const [artifactName, { resourceId }] of Object.entries(ecrOutputs)) {
|
|
2512
2526
|
const safeName = sanitizeResourceId(artifactName);
|
|
2513
2527
|
template.Outputs[`${safeName}RepoUri`] = {
|
|
@@ -2524,9 +2538,9 @@ function generateStageStackTemplate(options) {
|
|
|
2524
2538
|
}
|
|
2525
2539
|
return template;
|
|
2526
2540
|
}
|
|
2527
|
-
function buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, oidcProviderUrl) {
|
|
2541
|
+
function buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, oidcProviderUrl, additionalTrustedAccounts, skipOidc) {
|
|
2528
2542
|
const subject = `org:${orgSlug}/pipeline:${pipelineSlug}`;
|
|
2529
|
-
return buildOidcTrustPolicy(accountId, subject, oidcProviderUrl);
|
|
2543
|
+
return buildOidcTrustPolicy(accountId, subject, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
2530
2544
|
}
|
|
2531
2545
|
function buildStagePolicies(steps, additionalPolicies) {
|
|
2532
2546
|
const policies = [];
|
|
@@ -2595,12 +2609,12 @@ function buildStagePolicies(steps, additionalPolicies) {
|
|
|
2595
2609
|
|
|
2596
2610
|
// src/templates/import-stack.ts
|
|
2597
2611
|
function generateImportStackTemplate(options) {
|
|
2598
|
-
const { pipelineSlug, orgSlug, accountId, oidcProviderUrl } = options;
|
|
2612
|
+
const { pipelineSlug, orgSlug, accountId, oidcProviderUrl, additionalTrustedAccounts, skipOidc } = options;
|
|
2599
2613
|
const template = createBaseTemplate(
|
|
2600
2614
|
`DevRamps Import Stack for ${pipelineSlug} - grants read access for artifact imports`
|
|
2601
2615
|
);
|
|
2602
2616
|
const roleName = generateImportRoleName(pipelineSlug);
|
|
2603
|
-
const trustPolicy = buildOidcTrustPolicy(accountId, `org:${orgSlug}/cicd`, oidcProviderUrl);
|
|
2617
|
+
const trustPolicy = buildOidcTrustPolicy(accountId, `org:${orgSlug}/cicd`, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
2604
2618
|
const policies = buildImportRolePolicies();
|
|
2605
2619
|
template.Resources.ImportRole = createIamRoleResource(
|
|
2606
2620
|
roleName,
|
|
@@ -2728,6 +2742,15 @@ function getOidcProviderUrlFromEndpoint(endpointOverride) {
|
|
|
2728
2742
|
return void 0;
|
|
2729
2743
|
}
|
|
2730
2744
|
}
|
|
2745
|
+
function isLocalhostEndpoint(endpointOverride) {
|
|
2746
|
+
if (!endpointOverride) return false;
|
|
2747
|
+
try {
|
|
2748
|
+
const url = new URL(endpointOverride);
|
|
2749
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
2750
|
+
} catch {
|
|
2751
|
+
return false;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2731
2754
|
async function bootstrapCommand(options) {
|
|
2732
2755
|
try {
|
|
2733
2756
|
if (options.verbose) {
|
|
@@ -2778,7 +2801,12 @@ async function bootstrapCommand(options) {
|
|
|
2778
2801
|
process.exit(0);
|
|
2779
2802
|
}
|
|
2780
2803
|
const oidcProviderUrl = getOidcProviderUrlFromEndpoint(options.endpointOverride);
|
|
2781
|
-
|
|
2804
|
+
const additionalTrustedAccounts = options.additionalTrustedAccounts ? options.additionalTrustedAccounts.split(",").map((s) => s.trim()) : void 0;
|
|
2805
|
+
const skipOidc = isLocalhostEndpoint(options.endpointOverride);
|
|
2806
|
+
if (skipOidc) {
|
|
2807
|
+
info("Localhost endpoint detected \u2014 OIDC provider creation will be skipped");
|
|
2808
|
+
}
|
|
2809
|
+
await executeDeployment(plan, pipelines, pipelineArtifacts, authData, identity.accountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
2782
2810
|
} catch (error2) {
|
|
2783
2811
|
if (error2 instanceof DevRampsError) {
|
|
2784
2812
|
error(error2.message);
|
|
@@ -3051,48 +3079,53 @@ async function confirmDeploymentPlan(plan) {
|
|
|
3051
3079
|
]
|
|
3052
3080
|
});
|
|
3053
3081
|
}
|
|
3054
|
-
async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options, oidcProviderUrl) {
|
|
3082
|
+
async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc) {
|
|
3055
3083
|
const results = { success: 0, failed: 0 };
|
|
3056
3084
|
const remainingStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length + plan.importStacks.length;
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
success:
|
|
3075
|
-
|
|
3076
|
-
|
|
3085
|
+
if (skipOidc) {
|
|
3086
|
+
newline();
|
|
3087
|
+
header("Phase 1: Skipping Account Bootstrap Stacks (localhost endpoint, OIDC not needed)");
|
|
3088
|
+
} else {
|
|
3089
|
+
newline();
|
|
3090
|
+
header("Phase 1: Deploying Account Bootstrap Stacks");
|
|
3091
|
+
info(`Deploying ${plan.accountStacks.length} account stack(s) in parallel...`);
|
|
3092
|
+
newline();
|
|
3093
|
+
const accountProgress = getMultiStackProgress();
|
|
3094
|
+
for (const stack of plan.accountStacks) {
|
|
3095
|
+
accountProgress.addStack(stack.stackName, "account", stack.accountId, stack.region, 1);
|
|
3096
|
+
}
|
|
3097
|
+
accountProgress.start();
|
|
3098
|
+
const accountResults = await Promise.all(
|
|
3099
|
+
plan.accountStacks.map(async (stack) => {
|
|
3100
|
+
try {
|
|
3101
|
+
await deployAccountStack(stack, currentAccountId, options, oidcProviderUrl);
|
|
3102
|
+
return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
|
|
3103
|
+
} catch (error2) {
|
|
3104
|
+
return {
|
|
3105
|
+
stack: `${stack.stackName} (${stack.accountId})`,
|
|
3106
|
+
success: false,
|
|
3107
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
})
|
|
3111
|
+
);
|
|
3112
|
+
clearMultiStackProgress();
|
|
3113
|
+
newline();
|
|
3114
|
+
for (const result of accountResults) {
|
|
3115
|
+
if (result.success) {
|
|
3116
|
+
success(`${result.stack} deployed`);
|
|
3117
|
+
results.success++;
|
|
3118
|
+
} else {
|
|
3119
|
+
error(`${result.stack} failed: ${result.error}`);
|
|
3120
|
+
results.failed++;
|
|
3077
3121
|
}
|
|
3078
|
-
})
|
|
3079
|
-
);
|
|
3080
|
-
clearMultiStackProgress();
|
|
3081
|
-
newline();
|
|
3082
|
-
for (const result of accountResults) {
|
|
3083
|
-
if (result.success) {
|
|
3084
|
-
success(`${result.stack} deployed`);
|
|
3085
|
-
results.success++;
|
|
3086
|
-
} else {
|
|
3087
|
-
error(`${result.stack} failed: ${result.error}`);
|
|
3088
|
-
results.failed++;
|
|
3089
3122
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3123
|
+
if (results.failed > 0) {
|
|
3124
|
+
newline();
|
|
3125
|
+
header("Deployment Summary");
|
|
3126
|
+
error(`${results.failed} account stack(s) failed. Skipping remaining ${remainingStacks} stack(s).`);
|
|
3127
|
+
process.exit(1);
|
|
3128
|
+
}
|
|
3096
3129
|
}
|
|
3097
3130
|
newline();
|
|
3098
3131
|
header("Phase 2: Deploying Org, Pipeline, Stage, and Import Stacks");
|
|
@@ -3114,7 +3147,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
3114
3147
|
mainProgress.start();
|
|
3115
3148
|
const orgPromise = (async () => {
|
|
3116
3149
|
try {
|
|
3117
|
-
await deployOrgStack(plan, pipelines, authData, currentAccountId, options, oidcProviderUrl);
|
|
3150
|
+
await deployOrgStack(plan, pipelines, authData, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
3118
3151
|
return { stack: plan.orgStack.stackName, success: true };
|
|
3119
3152
|
} catch (error2) {
|
|
3120
3153
|
return {
|
|
@@ -3138,7 +3171,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
3138
3171
|
});
|
|
3139
3172
|
const stagePromises = plan.stageStacks.map(async (stack) => {
|
|
3140
3173
|
try {
|
|
3141
|
-
await deployStageStack(stack, authData, currentAccountId, options, oidcProviderUrl);
|
|
3174
|
+
await deployStageStack(stack, authData, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
3142
3175
|
return { stack: stack.stackName, success: true };
|
|
3143
3176
|
} catch (error2) {
|
|
3144
3177
|
return {
|
|
@@ -3150,7 +3183,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
3150
3183
|
});
|
|
3151
3184
|
const importPromises = plan.importStacks.map(async (stack) => {
|
|
3152
3185
|
try {
|
|
3153
|
-
await deployImportStack(stack, currentAccountId, options, oidcProviderUrl);
|
|
3186
|
+
await deployImportStack(stack, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc);
|
|
3154
3187
|
return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
|
|
3155
3188
|
} catch (error2) {
|
|
3156
3189
|
return {
|
|
@@ -3187,7 +3220,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
3187
3220
|
process.exit(1);
|
|
3188
3221
|
}
|
|
3189
3222
|
}
|
|
3190
|
-
async function deployOrgStack(plan, pipelines, authData, currentAccountId, options, oidcProviderUrl) {
|
|
3223
|
+
async function deployOrgStack(plan, pipelines, authData, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc) {
|
|
3191
3224
|
const { orgSlug, cicdAccountId, cicdRegion } = authData;
|
|
3192
3225
|
const credentials = cicdAccountId !== currentAccountId ? (await assumeRoleForAccount({
|
|
3193
3226
|
targetAccountId: cicdAccountId,
|
|
@@ -3218,7 +3251,9 @@ async function deployOrgStack(plan, pipelines, authData, currentAccountId, optio
|
|
|
3218
3251
|
orgSlug,
|
|
3219
3252
|
cicdAccountId,
|
|
3220
3253
|
targetAccountIds,
|
|
3221
|
-
oidcProviderUrl
|
|
3254
|
+
oidcProviderUrl,
|
|
3255
|
+
additionalTrustedAccounts,
|
|
3256
|
+
skipOidc
|
|
3222
3257
|
});
|
|
3223
3258
|
const deployOptions = {
|
|
3224
3259
|
stackName: plan.orgStack.stackName,
|
|
@@ -3270,7 +3305,7 @@ async function deployAccountStack(stack, currentAccountId, options, oidcProvider
|
|
|
3270
3305
|
await previewStackChanges(deployOptions);
|
|
3271
3306
|
await deployStack(deployOptions);
|
|
3272
3307
|
}
|
|
3273
|
-
async function deployStageStack(stack, authData, currentAccountId, options, oidcProviderUrl) {
|
|
3308
|
+
async function deployStageStack(stack, authData, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc) {
|
|
3274
3309
|
const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
|
|
3275
3310
|
targetAccountId: stack.accountId,
|
|
3276
3311
|
currentAccountId,
|
|
@@ -3285,7 +3320,9 @@ async function deployStageStack(stack, authData, currentAccountId, options, oidc
|
|
|
3285
3320
|
additionalPolicies: stack.additionalPolicies,
|
|
3286
3321
|
dockerArtifacts: stack.dockerArtifacts,
|
|
3287
3322
|
bundleArtifacts: stack.bundleArtifacts,
|
|
3288
|
-
oidcProviderUrl
|
|
3323
|
+
oidcProviderUrl,
|
|
3324
|
+
additionalTrustedAccounts,
|
|
3325
|
+
skipOidc
|
|
3289
3326
|
});
|
|
3290
3327
|
const deployOptions = {
|
|
3291
3328
|
stackName: stack.stackName,
|
|
@@ -3297,7 +3334,7 @@ async function deployStageStack(stack, authData, currentAccountId, options, oidc
|
|
|
3297
3334
|
await previewStackChanges(deployOptions);
|
|
3298
3335
|
await deployStack(deployOptions);
|
|
3299
3336
|
}
|
|
3300
|
-
async function deployImportStack(stack, currentAccountId, options, oidcProviderUrl) {
|
|
3337
|
+
async function deployImportStack(stack, currentAccountId, options, oidcProviderUrl, additionalTrustedAccounts, skipOidc) {
|
|
3301
3338
|
const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
|
|
3302
3339
|
targetAccountId: stack.accountId,
|
|
3303
3340
|
currentAccountId,
|
|
@@ -3307,7 +3344,9 @@ async function deployImportStack(stack, currentAccountId, options, oidcProviderU
|
|
|
3307
3344
|
pipelineSlug: stack.pipelineSlug,
|
|
3308
3345
|
orgSlug: stack.orgSlug,
|
|
3309
3346
|
accountId: stack.accountId,
|
|
3310
|
-
oidcProviderUrl
|
|
3347
|
+
oidcProviderUrl,
|
|
3348
|
+
additionalTrustedAccounts,
|
|
3349
|
+
skipOidc
|
|
3311
3350
|
});
|
|
3312
3351
|
const deployOptions = {
|
|
3313
3352
|
stackName: stack.stackName,
|
|
@@ -3337,5 +3376,8 @@ program.command("bootstrap").description("Bootstrap IAM roles in target AWS acco
|
|
|
3337
3376
|
).option(
|
|
3338
3377
|
"--endpoint-override <url>",
|
|
3339
3378
|
"Override the DevRamps API endpoint (for testing, e.g., http://localhost:3000)"
|
|
3379
|
+
).option(
|
|
3380
|
+
"--additional-trusted-accounts <accounts>",
|
|
3381
|
+
"Comma-separated AWS account IDs to add to role trust policies (for local dev testing)"
|
|
3340
3382
|
).action(bootstrapCommand);
|
|
3341
3383
|
program.parse();
|