@go-to-k/cdkd 0.120.0 → 0.122.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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-CuHRHcyW.js";
3
- import { $ as RouteDiscoveryError, A as runDockerStreaming, B as AssemblyReader, C as AssetPublisher, D as formatDockerLoginError, E as buildDockerImage, F as resolveCaptureObservedState, H as resolveBucketRegion, I as resolveSkipPrefix, L as resolveStateBucketWithDefault, M as getDefaultStateBucketName, N as getLegacyStateBucketName, O as getDockerCmd, P as resolveApp, Q as ResourceUpdateNotSupportedError, R as resolveStateBucketWithDefaultAndSource, S as shouldRetainResource, T as WorkGraph, W as CdkdError, X as ProvisioningError, Y as PartialFailureError, Z as ResourceTimeoutError, _ as DiffCalculator, _t as withSkipPrefix, a as withRetry, b as LockManager, c as collectInlinePolicyNamesManagedBySiblings, d as normalizeAwsTagsToCfn, dt as runStackBuffered, et as StackHasActiveImportsError, f as resolveExplicitPhysicalId, ft as getLiveRenderer, g as IntrinsicFunctionResolver, gt as generateResourceNameWithFallback, h as assertRegionMatch, ht as generateResourceName, i as withResourceDeadline, j as Synthesizer, k as runDockerForeground, l as CDK_PATH_TAG, lt as getLogger, m as CloudControlProvider, mt as PATTERN_B_RESOURCE_TYPES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as IMPLICIT_DELETE_DEPENDENCIES, ot as normalizeAwsError, p as ProviderRegistry, pt as PATTERN_B_NAME_PROPERTIES, q as LocalInvokeBuildError, r as DeployEngine, s as IAMRoleProvider, st as withErrorHandling, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as StackTerminationProtectionError, u as matchesCdkPath, v as DagBuilder, vt as withStackName, w as stringifyValue, x as S3StateBackend, y as TemplateParser, z as warnDeprecatedNoPrefixCliFlag } from "./deploy-engine-Chzg_hDE.js";
3
+ import { $ as LocalInvokeBuildError, A as stringifyValue, B as resolveApp, C as DiffCalculator, Ct as withSkipPrefix, D as S3StateBackend, E as LockManager, F as runDockerForeground, G as warnDeprecatedNoPrefixCliFlag, H as resolveSkipPrefix, I as runDockerStreaming, J as resolveBucketRegion, K as AssemblyReader, L as Synthesizer, M as buildDockerImage, N as formatDockerLoginError, O as shouldRetainResource, P as getDockerCmd, R as getDefaultStateBucketName, S as IntrinsicFunctionResolver, St as generateResourceNameWithFallback, T as TemplateParser, U as resolveStateBucketWithDefault, V as resolveCaptureObservedState, W as resolveStateBucketWithDefaultAndSource, X as CdkdError, _ as normalizeAwsTagsToCfn, _t as runStackBuffered, a as withRetry, at as RouteDiscoveryError, b as CloudControlProvider, bt as PATTERN_B_RESOURCE_TYPES, c as cyan, d as red, f as yellow, ft as normalizeAwsError, g as matchesCdkPath, h as CDK_PATH_TAG, ht as getLogger, i as withResourceDeadline, it as ResourceUpdateNotSupportedError, j as WorkGraph, k as AssetPublisher, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ProvisioningError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as StackHasActiveImportsError, p as IAMRoleProvider, pt as withErrorHandling, r as DeployEngine, rt as ResourceTimeoutError, s as bold, st as StackTerminationProtectionError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as PartialFailureError, u as green, v as resolveExplicitPhysicalId, vt as getLiveRenderer, w as DagBuilder, wt as withStackName, x as assertRegionMatch, xt as generateResourceName, y as ProviderRegistry, yt as PATTERN_B_NAME_PROPERTIES, z as getLegacyStateBucketName } from "./deploy-engine-B2RZT3ai.js";
4
4
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
6
6
  import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachUserPolicyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachUserPolicyCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListInstanceProfilesCommand, ListUserPoliciesCommand, ListUserTagsCommand, ListUsersCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagUserCommand, UntagUserCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
@@ -696,7 +696,7 @@ async function synthCommand(options) {
696
696
  const template = stacks[0].template;
697
697
  process.stdout.write(toYaml(template));
698
698
  }
699
- logger.info(`\n Synthesis complete! Found ${stacks.length} stack(s):`);
699
+ logger.info(`\n${green("✓")} ${bold("Synthesis complete!")} Found ${stacks.length} stack(s):`);
700
700
  for (const stack of stacks) {
701
701
  const resourceCount = countDeployableResources(stack.template);
702
702
  const outputCount = Object.keys(stack.template.Outputs ?? {}).length;
@@ -30909,7 +30909,7 @@ async function deployCommand(stacks, options) {
30909
30909
  };
30910
30910
  process.on("SIGINT", topLevelSigintHandler);
30911
30911
  try {
30912
- logger.info("Synthesizing CDK app...");
30912
+ logger.info(cyan("Synthesizing CDK app..."));
30913
30913
  const synthesizer = new Synthesizer();
30914
30914
  const context = parseContextOptions(options.context);
30915
30915
  const { stacks: allStacks } = await synthesizer.synthesize({
@@ -30995,7 +30995,7 @@ async function deployCommand(stacks, options) {
30995
30995
  };
30996
30996
  const runStackInner = async (stackInfo) => {
30997
30997
  const stackRegion = stackInfo.region || baseRegion;
30998
- logger.info(`\nDeploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region: ${stackRegion})` : ""}`);
30998
+ logger.info(`\n${cyan("Deploying stack:")} ${bold(cyan(stackInfo.stackName))}${stackRegion !== baseRegion ? gray(` (region: ${stackRegion})`) : ""}`);
30999
30999
  switchRegion(stackRegion);
31000
31000
  const stackAwsClients = new AwsClients({
31001
31001
  region: stackRegion,
@@ -31032,19 +31032,19 @@ async function deployCommand(stacks, options) {
31032
31032
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
31033
31033
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
31034
31034
  }, stackRegion, exportIndexStore).deploy(stackInfo.stackName, stackInfo.template);
31035
- logger.info("\nDeployment Summary:");
31036
- logger.info(` Stack: ${deployResult.stackName}`);
31037
- logger.info(` Created: ${deployResult.created}`);
31038
- logger.info(` Updated: ${deployResult.updated}`);
31039
- logger.info(` Deleted: ${deployResult.deleted}`);
31040
- logger.info(` Unchanged: ${deployResult.unchanged}`);
31041
- logger.info(` Duration: ${(deployResult.durationMs / 1e3).toFixed(2)}s`);
31035
+ logger.info(`\n${bold("Deployment Summary:")}`);
31036
+ logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
31037
+ logger.info(` Created: ${deployResult.created > 0 ? green(deployResult.created) : gray(deployResult.created)}`);
31038
+ logger.info(` Updated: ${deployResult.updated > 0 ? yellow(deployResult.updated) : gray(deployResult.updated)}`);
31039
+ logger.info(` Deleted: ${deployResult.deleted > 0 ? red(deployResult.deleted) : gray(deployResult.deleted)}`);
31040
+ logger.info(` Unchanged: ${gray(deployResult.unchanged)}`);
31041
+ logger.info(` Duration: ${cyan((deployResult.durationMs / 1e3).toFixed(2) + "s")}`);
31042
31042
  if (deployResult.outputs && Object.keys(deployResult.outputs).length > 0) {
31043
31043
  logger.info("\nOutputs:");
31044
31044
  for (const [key, value] of Object.entries(deployResult.outputs)) logger.info(` ${deployResult.stackName}.${key} = ${String(value)}`);
31045
31045
  }
31046
- if (options.dryRun) logger.info("\n✓ Dry run completed - no actual changes made");
31047
- else logger.info("\n✓ Deployment completed successfully");
31046
+ if (options.dryRun) logger.info(`\n${green("✓")} ${bold("Dry run completed")} - no actual changes made`);
31047
+ else logger.info(`\n${green("✓")} ${bold("Deployment completed successfully")}`);
31048
31048
  } finally {
31049
31049
  stackAwsClients.destroy();
31050
31050
  stateS3Client.destroy();
@@ -32239,7 +32239,7 @@ async function runDestroyForStack(stackName, state, ctx) {
32239
32239
  if (resourceCount === 0) {
32240
32240
  logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
32241
32241
  await ctx.stateBackend.deleteState(stackName, regionForState);
32242
- logger.info("✓ State deleted");
32242
+ logger.info(`${green("✓")} State deleted`);
32243
32243
  result.skippedEmpty = true;
32244
32244
  return result;
32245
32245
  }
@@ -32399,7 +32399,7 @@ async function runDestroyForStack(stackName, state, ctx) {
32399
32399
  onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
32400
32400
  });
32401
32401
  renderer.removeTask(logicalId);
32402
- logger.info(` ${logicalId} (${resource.resourceType}) deleted`);
32402
+ logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
32403
32403
  result.deletedCount++;
32404
32404
  } catch (error) {
32405
32405
  renderer.removeTask(logicalId);
@@ -32427,8 +32427,8 @@ async function runDestroyForStack(stackName, state, ctx) {
32427
32427
  if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
32428
32428
  } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
32429
32429
  const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
32430
- if (result.errorCount === 0) logger.info(`\n✓ Stack ${stackName} destroyed (${result.deletedCount} deleted${retainedSuffix}, ${result.errorCount} errors)`);
32431
- else logger.warn(`\n⚠ Stack ${stackName} partially destroyed (${result.deletedCount} deleted${retainedSuffix}, ${result.errorCount} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
32430
+ if (result.errorCount === 0) logger.info(`\n${green("")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
32431
+ else logger.warn(`\n${yellow("")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
32432
32432
  } finally {
32433
32433
  renderer.stop();
32434
32434
  logger.debug("Releasing lock...");
@@ -33466,7 +33466,7 @@ async function publishAssetsCommand(stacks, options) {
33466
33466
  logger.info(` ${tag} ${id} — ${detail}`);
33467
33467
  }
33468
33468
  if (failed.length > 0) throw new PartialFailureError(`Asset publishing completed with ${failed.length} stack failure(s) (${totalAssets} asset(s) published successfully across the rest).`);
33469
- logger.info(`\n Asset publishing complete (${totalAssets} asset(s))`);
33469
+ logger.info(`\n${green("✓")} ${bold("Asset publishing complete")} (${totalAssets} asset(s))`);
33470
33470
  }
33471
33471
  /**
33472
33472
  * Create publish-assets command.
@@ -35768,6 +35768,119 @@ async function loadStateForStack(stackName, synthRegion, opts) {
35768
35768
  resetAwsClients();
35769
35769
  }
35770
35770
  }
35771
+ /**
35772
+ * Build a {@link CrossStackResolver} that walks cdkd's S3 state to look
35773
+ * up `Fn::ImportValue` / `Fn::GetStackOutput` references the same way
35774
+ * `cdkd deploy`'s `IntrinsicFunctionResolver` does. Returns `undefined`
35775
+ * when the state bucket cannot be resolved (warn + fall back; matches
35776
+ * `loadStateForStack`'s policy).
35777
+ *
35778
+ * The returned `dispose` closes the AWS clients owned by the resolver
35779
+ * when the caller is done — callers MUST call it (typically in a
35780
+ * `try / finally`) so the per-request S3 client isn't leaked across the
35781
+ * CLI's lifetime.
35782
+ *
35783
+ * Why a separate AwsClients instance from `loadStateForStack`: the
35784
+ * existing helper destroys its clients in a `finally` immediately after
35785
+ * loading the consumer stack's state. The cross-stack resolver lives
35786
+ * longer — every env-var that references a cross-stack output triggers a
35787
+ * new state read. Owning a fresh `AwsClients` here gives the resolver
35788
+ * an independent lifetime managed by the caller.
35789
+ *
35790
+ * Same-account / same-region only in v1 (the resolver's `producerRegion`
35791
+ * arg is honored, but only for state lookups within the same cdkd state
35792
+ * bucket). Cross-region `Fn::ImportValue` is tracked under #451;
35793
+ * cross-account `Fn::GetStackOutput.RoleArn` is tracked under #449.
35794
+ */
35795
+ async function buildCrossStackResolver(consumerRegion, opts) {
35796
+ const logger = getLogger();
35797
+ const prefix = opts.logPrefix ?? "--from-state";
35798
+ let stateBucket;
35799
+ try {
35800
+ stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, consumerRegion);
35801
+ } catch (err) {
35802
+ logger.warn(`${prefix}: cross-stack resolver could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
35803
+ return;
35804
+ }
35805
+ const awsClients = new AwsClients({
35806
+ ...opts.region !== void 0 && { region: opts.region },
35807
+ ...opts.profile !== void 0 && { profile: opts.profile }
35808
+ });
35809
+ const stateConfig = {
35810
+ bucket: stateBucket,
35811
+ prefix: opts.statePrefix
35812
+ };
35813
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
35814
+ ...opts.region !== void 0 && { region: opts.region },
35815
+ ...opts.profile !== void 0 && { profile: opts.profile }
35816
+ });
35817
+ try {
35818
+ await stateBackend.verifyBucketExists();
35819
+ } catch (err) {
35820
+ awsClients.destroy();
35821
+ logger.warn(`${prefix}: cross-stack resolver could not access state bucket '${stateBucket}': ${err instanceof Error ? err.message : String(err)}. Fn::ImportValue / Fn::GetStackOutput env entries will warn-and-drop.`);
35822
+ return;
35823
+ }
35824
+ const exportIndex = new ExportIndexStore(awsClients.s3, stateBucket, opts.statePrefix, consumerRegion, stateBackend);
35825
+ return {
35826
+ resolver: {
35827
+ async resolveImport(exportName) {
35828
+ try {
35829
+ const entry = await exportIndex.lookup(exportName);
35830
+ if (entry) {
35831
+ const value = entry.value;
35832
+ if (typeof value === "string") return value;
35833
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
35834
+ return JSON.stringify(value);
35835
+ }
35836
+ } catch (err) {
35837
+ logger.debug(`${prefix}: exports index lookup failed for '${exportName}': ${err instanceof Error ? err.message : String(err)}; falling back to per-stack state scan`);
35838
+ }
35839
+ let refs;
35840
+ try {
35841
+ refs = await stateBackend.listStacks();
35842
+ } catch (err) {
35843
+ logger.debug(`${prefix}: failed to list stacks during Fn::ImportValue fallback for '${exportName}': ${err instanceof Error ? err.message : String(err)}`);
35844
+ return;
35845
+ }
35846
+ for (const ref of refs) {
35847
+ const region = ref.region ?? consumerRegion;
35848
+ if (region !== consumerRegion) continue;
35849
+ try {
35850
+ const got = await stateBackend.getState(ref.stackName, region);
35851
+ if (!got || !got.state.outputs) continue;
35852
+ if (exportName in got.state.outputs) {
35853
+ const value = got.state.outputs[exportName];
35854
+ if (typeof value === "string") return value;
35855
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
35856
+ return JSON.stringify(value);
35857
+ }
35858
+ } catch (err) {
35859
+ logger.debug(`${prefix}: state read failed for ${ref.stackName} (${region}) during Fn::ImportValue fallback: ${err instanceof Error ? err.message : String(err)}`);
35860
+ continue;
35861
+ }
35862
+ }
35863
+ },
35864
+ async resolveGetStackOutput(producerStack, producerRegion, outputName) {
35865
+ try {
35866
+ const got = await stateBackend.getState(producerStack, producerRegion);
35867
+ if (!got || !got.state.outputs) return void 0;
35868
+ if (!(outputName in got.state.outputs)) return void 0;
35869
+ const value = got.state.outputs[outputName];
35870
+ if (typeof value === "string") return value;
35871
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
35872
+ return JSON.stringify(value);
35873
+ } catch (err) {
35874
+ logger.debug(`${prefix}: state read failed for Fn::GetStackOutput '${producerStack}.${outputName}' (${producerRegion}): ${err instanceof Error ? err.message : String(err)}`);
35875
+ return;
35876
+ }
35877
+ }
35878
+ },
35879
+ dispose: () => {
35880
+ awsClients.destroy();
35881
+ }
35882
+ };
35883
+ }
35771
35884
 
35772
35885
  //#endregion
35773
35886
  //#region src/local/intrinsic-image.ts
@@ -36733,6 +36846,159 @@ function resolveJoin(arg, context) {
36733
36846
  };
36734
36847
  }
36735
36848
  /**
36849
+ * Async sibling of {@link substituteAgainstState}. Same semantics for every
36850
+ * intrinsic the sync path supports; additionally consults the
36851
+ * `crossStackResolver` (when supplied on the context) for `Fn::ImportValue`
36852
+ * and `Fn::GetStackOutput`.
36853
+ *
36854
+ * Callers that don't need cross-stack support should keep using the sync
36855
+ * helper. Code paths that wire `--from-state` env / secret substitution
36856
+ * (e.g. `cdkd local invoke --from-state`, `cdkd local run-task --from-state`)
36857
+ * route through this async version so a single env-var referencing a
36858
+ * cross-stack output is no longer warn-and-dropped.
36859
+ */
36860
+ async function substituteAgainstStateAsync(value, contextOrResources) {
36861
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
36862
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return {
36863
+ kind: "literal",
36864
+ value
36865
+ };
36866
+ if (value === null || typeof value !== "object") return {
36867
+ kind: "unresolved",
36868
+ reason: `unsupported value type: ${value === null ? "null" : typeof value}`
36869
+ };
36870
+ const obj = value;
36871
+ const keys = Object.keys(obj);
36872
+ if (keys.length !== 1) return {
36873
+ kind: "unresolved",
36874
+ reason: `expected an intrinsic with one key, got ${keys.length} keys`
36875
+ };
36876
+ const intrinsic = keys[0];
36877
+ const arg = obj[intrinsic];
36878
+ if (intrinsic === "Ref" || intrinsic === "Fn::GetAtt" || intrinsic === "Fn::Sub" || intrinsic === "Fn::Join") return substituteAgainstState(value, context);
36879
+ if (intrinsic === "Fn::ImportValue") return resolveImportValueAsync(arg, context);
36880
+ if (intrinsic === "Fn::GetStackOutput") return resolveGetStackOutputAsync(arg, context);
36881
+ return {
36882
+ kind: "unresolved",
36883
+ reason: `unsupported intrinsic '${intrinsic}' (supported: Ref, Fn::GetAtt, Fn::Sub, Fn::Join, Fn::ImportValue, Fn::GetStackOutput)`
36884
+ };
36885
+ }
36886
+ /**
36887
+ * `Fn::ImportValue: <exportName>` — the argument may itself be an
36888
+ * intrinsic that resolves to a string (e.g.
36889
+ * `{Fn::ImportValue: {Fn::Sub: 'MyStack-${AWS::Region}-Bucket'}}` — the
36890
+ * inner `Fn::Sub` is resolved against `pseudoParameters` first, then the
36891
+ * resulting string is looked up via the cross-stack resolver).
36892
+ */
36893
+ async function resolveImportValueAsync(arg, context) {
36894
+ const inner = substituteAgainstState(arg, context);
36895
+ if (inner.kind !== "literal") return {
36896
+ kind: "unresolved",
36897
+ reason: `Fn::ImportValue argument: ${inner.reason}`
36898
+ };
36899
+ if (typeof inner.value !== "string" || inner.value.length === 0) return {
36900
+ kind: "unresolved",
36901
+ reason: `Fn::ImportValue argument must resolve to a non-empty string, got ${typeof inner.value}`
36902
+ };
36903
+ const exportName = inner.value;
36904
+ if (!context.crossStackResolver) return {
36905
+ kind: "unresolved",
36906
+ reason: `Fn::ImportValue '${exportName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
36907
+ };
36908
+ let resolved;
36909
+ try {
36910
+ resolved = await context.crossStackResolver.resolveImport(exportName);
36911
+ } catch (err) {
36912
+ return {
36913
+ kind: "unresolved",
36914
+ reason: `Fn::ImportValue '${exportName}': lookup failed: ${err instanceof Error ? err.message : String(err)}`
36915
+ };
36916
+ }
36917
+ if (resolved === void 0) return {
36918
+ kind: "unresolved",
36919
+ reason: `Fn::ImportValue '${exportName}': export not found in any cdkd-managed stack in this region`
36920
+ };
36921
+ return {
36922
+ kind: "literal",
36923
+ value: resolved
36924
+ };
36925
+ }
36926
+ /**
36927
+ * `Fn::GetStackOutput: { StackName, OutputName, Region? }`. Same shape as
36928
+ * the deploy-engine resolver. `RoleArn` (cross-account) is intentionally
36929
+ * NOT supported in this path — the user-visible error message points at
36930
+ * the followup issue tracking it.
36931
+ */
36932
+ async function resolveGetStackOutputAsync(arg, context) {
36933
+ if (!arg || typeof arg !== "object" || Array.isArray(arg)) return {
36934
+ kind: "unresolved",
36935
+ reason: `Fn::GetStackOutput argument must be an object with StackName / OutputName / Region, got ${arg === null ? "null" : Array.isArray(arg) ? "array" : typeof arg}`
36936
+ };
36937
+ const args = arg;
36938
+ const stackNameSub = substituteAgainstState(args["StackName"], context);
36939
+ if (stackNameSub.kind !== "literal") return {
36940
+ kind: "unresolved",
36941
+ reason: `Fn::GetStackOutput.StackName: ${stackNameSub.reason}`
36942
+ };
36943
+ if (typeof stackNameSub.value !== "string" || stackNameSub.value.length === 0) return {
36944
+ kind: "unresolved",
36945
+ reason: `Fn::GetStackOutput.StackName must resolve to a non-empty string, got ${typeof stackNameSub.value}`
36946
+ };
36947
+ const stackName = stackNameSub.value;
36948
+ const outputNameSub = substituteAgainstState(args["OutputName"], context);
36949
+ if (outputNameSub.kind !== "literal") return {
36950
+ kind: "unresolved",
36951
+ reason: `Fn::GetStackOutput.OutputName: ${outputNameSub.reason}`
36952
+ };
36953
+ if (typeof outputNameSub.value !== "string" || outputNameSub.value.length === 0) return {
36954
+ kind: "unresolved",
36955
+ reason: `Fn::GetStackOutput.OutputName must resolve to a non-empty string, got ${typeof outputNameSub.value}`
36956
+ };
36957
+ const outputName = outputNameSub.value;
36958
+ let region;
36959
+ if (args["Region"] !== void 0 && args["Region"] !== null) {
36960
+ const regionSub = substituteAgainstState(args["Region"], context);
36961
+ if (regionSub.kind !== "literal") return {
36962
+ kind: "unresolved",
36963
+ reason: `Fn::GetStackOutput.Region: ${regionSub.reason}`
36964
+ };
36965
+ if (typeof regionSub.value !== "string" || regionSub.value.length === 0) return {
36966
+ kind: "unresolved",
36967
+ reason: `Fn::GetStackOutput.Region must resolve to a non-empty string, got ${typeof regionSub.value}`
36968
+ };
36969
+ region = regionSub.value;
36970
+ } else region = context.consumerRegion ?? context.pseudoParameters?.region;
36971
+ if (!region) return {
36972
+ kind: "unresolved",
36973
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': no Region supplied and consumer region is unknown (set --region, AWS_REGION, or env.region on the CDK stack)`
36974
+ };
36975
+ if (args["RoleArn"] !== void 0 && args["RoleArn"] !== null) return {
36976
+ kind: "unresolved",
36977
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': RoleArn (cross-account) is not yet supported by --from-state — tracked under issue #449`
36978
+ };
36979
+ if (!context.crossStackResolver) return {
36980
+ kind: "unresolved",
36981
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}': no cross-stack resolver supplied (pass --from-state and ensure the producer stack was deployed via cdkd deploy)`
36982
+ };
36983
+ let resolved;
36984
+ try {
36985
+ resolved = await context.crossStackResolver.resolveGetStackOutput(stackName, region, outputName);
36986
+ } catch (err) {
36987
+ return {
36988
+ kind: "unresolved",
36989
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): lookup failed: ${err instanceof Error ? err.message : String(err)}`
36990
+ };
36991
+ }
36992
+ if (resolved === void 0) return {
36993
+ kind: "unresolved",
36994
+ reason: `Fn::GetStackOutput '${stackName}.${outputName}' (${region}): output not found in producer stack state`
36995
+ };
36996
+ return {
36997
+ kind: "literal",
36998
+ value: resolved
36999
+ };
37000
+ }
37001
+ /**
36736
37002
  * Build a pre-substituted env map from the template entry by feeding each
36737
37003
  * intrinsic value through `substituteAgainstState`. Literal entries pass
36738
37004
  * through untouched (the env-resolver handles them).
@@ -36773,6 +37039,50 @@ function substituteEnvVarsFromState(templateEnv, contextOrResources) {
36773
37039
  audit
36774
37040
  };
36775
37041
  }
37042
+ /**
37043
+ * Async sibling of {@link substituteEnvVarsFromState}. Routes every
37044
+ * intrinsic-valued entry through {@link substituteAgainstStateAsync} so
37045
+ * `Fn::ImportValue` / `Fn::GetStackOutput` resolve via the context's
37046
+ * `crossStackResolver` (when supplied). Mirrors the sync version in every
37047
+ * other respect: literal entries pass through unchanged, unresolved
37048
+ * entries are dropped with a per-key audit reason, and the env-resolver
37049
+ * sees the same "no template value" shape so the warn-and-drop path
37050
+ * fires consistently.
37051
+ *
37052
+ * Closes issue #454 — `cdkd local invoke --from-state` and
37053
+ * `cdkd local run-task --from-state` can now resolve cross-stack output
37054
+ * references in env vars / secrets instead of warn-and-dropping them.
37055
+ */
37056
+ async function substituteEnvVarsFromStateAsync(templateEnv, contextOrResources) {
37057
+ const env = {};
37058
+ const audit = {
37059
+ resolvedKeys: [],
37060
+ unresolved: []
37061
+ };
37062
+ if (!templateEnv) return {
37063
+ env,
37064
+ audit
37065
+ };
37066
+ const context = isContext(contextOrResources) ? contextOrResources : { resources: contextOrResources };
37067
+ for (const [key, value] of Object.entries(templateEnv)) {
37068
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
37069
+ env[key] = value;
37070
+ continue;
37071
+ }
37072
+ const result = await substituteAgainstStateAsync(value, context);
37073
+ if (result.kind === "literal") {
37074
+ env[key] = result.value;
37075
+ audit.resolvedKeys.push(key);
37076
+ } else audit.unresolved.push({
37077
+ key,
37078
+ reason: result.reason
37079
+ });
37080
+ }
37081
+ return {
37082
+ env,
37083
+ audit
37084
+ };
37085
+ }
36776
37086
 
36777
37087
  //#endregion
36778
37088
  //#region src/local/ecs-task-resolver.ts
@@ -36816,6 +37126,7 @@ function detectEcsImageResolutionNeeds(stack) {
36816
37126
  let needsPseudoParameters = false;
36817
37127
  let needsStateResources = false;
36818
37128
  let needsEnvOrSecretSubstitution = false;
37129
+ let needsCrossStackResolver = false;
36819
37130
  for (const res of Object.values(resources)) {
36820
37131
  if (res.Type !== "AWS::ECS::TaskDefinition") continue;
36821
37132
  const props = res.Properties ?? {};
@@ -36828,6 +37139,10 @@ function detectEcsImageResolutionNeeds(stack) {
36828
37139
  if (need.pseudo) needsPseudoParameters = true;
36829
37140
  if (need.state) needsStateResources = true;
36830
37141
  if (containerHasIntrinsicEnvOrSecret(co)) needsEnvOrSecretSubstitution = true;
37142
+ if (containerHasCrossStackEnvOrSecret(co)) {
37143
+ needsEnvOrSecretSubstitution = true;
37144
+ needsCrossStackResolver = true;
37145
+ }
36831
37146
  }
36832
37147
  const rawVolumes = props["Volumes"];
36833
37148
  if (Array.isArray(rawVolumes)) {
@@ -36841,10 +37156,37 @@ function detectEcsImageResolutionNeeds(stack) {
36841
37156
  return {
36842
37157
  needsPseudoParameters,
36843
37158
  needsStateResources,
36844
- needsEnvOrSecretSubstitution
37159
+ needsEnvOrSecretSubstitution,
37160
+ needsCrossStackResolver
36845
37161
  };
36846
37162
  }
36847
37163
  /**
37164
+ * Returns true when any `Environment[].Value` or `Secrets[].ValueFrom`
37165
+ * is a top-level `Fn::ImportValue` / `Fn::GetStackOutput` intrinsic.
37166
+ * Issue #454 — gates cross-stack resolver construction so literal +
37167
+ * same-stack-intrinsic env / secret maps don't pay the extra cost.
37168
+ */
37169
+ function containerHasCrossStackEnvOrSecret(c) {
37170
+ const env = c["Environment"];
37171
+ if (Array.isArray(env)) for (const entry of env) {
37172
+ if (!entry || typeof entry !== "object") continue;
37173
+ const v = entry["Value"];
37174
+ if (isCrossStackIntrinsic(v)) return true;
37175
+ }
37176
+ const secrets = c["Secrets"];
37177
+ if (Array.isArray(secrets)) for (const entry of secrets) {
37178
+ if (!entry || typeof entry !== "object") continue;
37179
+ const v = entry["ValueFrom"];
37180
+ if (isCrossStackIntrinsic(v)) return true;
37181
+ }
37182
+ return false;
37183
+ }
37184
+ function isCrossStackIntrinsic(value) {
37185
+ if (!value || typeof value !== "object") return false;
37186
+ const obj = value;
37187
+ return "Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj;
37188
+ }
37189
+ /**
36848
37190
  * Detect whether a Volume entry has an intrinsic-valued `Host.SourcePath`.
36849
37191
  * Used by `detectEcsImageResolutionNeeds` (Gap 6 of #286) to trigger
36850
37192
  * state-load + pseudo-parameter resolution under `--from-state` when the
@@ -37589,6 +37931,87 @@ function checkVolumeHostPath(hostPath) {
37589
37931
  return false;
37590
37932
  }
37591
37933
  }
37934
+ /**
37935
+ * Async post-pass that walks the resolved task's container Environment +
37936
+ * Secrets entries from the raw template (preserved in `task.resource`)
37937
+ * and re-attempts substitution via {@link substituteAgainstStateAsync}
37938
+ * against the supplied context. The sync `parseContainerDefinition`
37939
+ * pass already substituted everything the legacy resolver handles; this
37940
+ * pass picks up the additional shapes the async resolver supports —
37941
+ * specifically `Fn::ImportValue` / `Fn::GetStackOutput` (issue #454).
37942
+ *
37943
+ * Resolved entries are patched onto the container's `environment` /
37944
+ * `secrets` map AND the corresponding `warnings` entries are filtered
37945
+ * out so the CLI doesn't print a stale per-container warn for an entry
37946
+ * the post-pass successfully resolved. Entries that STILL can't resolve
37947
+ * (e.g. producer stack not deployed) keep their original warning so the
37948
+ * CLI's UX matches the sync path.
37949
+ *
37950
+ * Pure-functional on the task structure outside of in-place mutation of
37951
+ * the container's `environment` / `secrets` / `warnings` arrays. The
37952
+ * task is expected to be the same `ResolvedEcsTask` instance returned
37953
+ * by `resolveEcsTaskTarget` — the runner downstream reads from these
37954
+ * fields directly.
37955
+ */
37956
+ async function applyCrossStackResolverToTask(task, context) {
37957
+ if (!context.crossStackResolver) return;
37958
+ const rawContainers = (task.resource.Properties ?? {})["ContainerDefinitions"];
37959
+ if (!Array.isArray(rawContainers)) return;
37960
+ for (let idx = 0; idx < task.containers.length; idx += 1) {
37961
+ const container = task.containers[idx];
37962
+ const raw = rawContainers[idx];
37963
+ if (!raw || typeof raw !== "object") continue;
37964
+ const c = raw;
37965
+ const containerName = pickString(c["Name"]) ?? container.name;
37966
+ const resolvedEnvKeys = /* @__PURE__ */ new Set();
37967
+ const resolvedSecretNames = /* @__PURE__ */ new Set();
37968
+ if (Array.isArray(c["Environment"])) for (const entry of c["Environment"]) {
37969
+ if (!entry || typeof entry !== "object") continue;
37970
+ const e = entry;
37971
+ const key = pickString(e["Name"]);
37972
+ const value = e["Value"];
37973
+ if (!key) continue;
37974
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") continue;
37975
+ if (key in container.environment) continue;
37976
+ if (!isCrossStackIntrinsic(value)) continue;
37977
+ const sub = await substituteAgainstStateAsync(value, context);
37978
+ if (sub.kind === "literal") {
37979
+ container.environment[key] = String(sub.value);
37980
+ resolvedEnvKeys.add(key);
37981
+ }
37982
+ }
37983
+ if (Array.isArray(c["Secrets"])) for (const entry of c["Secrets"]) {
37984
+ if (!entry || typeof entry !== "object") continue;
37985
+ const e = entry;
37986
+ const sName = pickString(e["Name"]);
37987
+ const valueFromRaw = e["ValueFrom"];
37988
+ if (!sName) continue;
37989
+ if (typeof valueFromRaw === "string" && valueFromRaw.length > 0) continue;
37990
+ if (container.secrets.some((s) => s.name === sName)) continue;
37991
+ if (!isCrossStackIntrinsic(valueFromRaw)) continue;
37992
+ const sub = await substituteAgainstStateAsync(valueFromRaw, context);
37993
+ if (sub.kind === "literal" && typeof sub.value === "string" && sub.value.length > 0) {
37994
+ container.secrets.push({
37995
+ name: sName,
37996
+ valueFrom: sub.value
37997
+ });
37998
+ resolvedSecretNames.add(sName);
37999
+ }
38000
+ }
38001
+ if (resolvedEnvKeys.size > 0 || resolvedSecretNames.size > 0) {
38002
+ container.warnings = container.warnings.filter((w) => {
38003
+ for (const k of resolvedEnvKeys) if (w.startsWith(`Environment '${k}' dropped:`)) return false;
38004
+ for (const k of resolvedSecretNames) if (w.startsWith(`Secret '${k}' dropped:`)) return false;
38005
+ return true;
38006
+ });
38007
+ task.warnings = task.warnings.filter((w) => {
38008
+ for (const k of resolvedEnvKeys) if (w.startsWith(`Container '${containerName}': Environment '${k}' dropped:`)) return false;
38009
+ for (const k of resolvedSecretNames) if (w.startsWith(`Container '${containerName}': Secret '${k}' dropped:`)) return false;
38010
+ return true;
38011
+ });
38012
+ }
38013
+ }
38014
+ }
37592
38015
 
37593
38016
  //#endregion
37594
38017
  //#region src/local/runtime-image.ts
@@ -44605,8 +45028,34 @@ async function localRunTaskCommand(target, options) {
44605
45028
  ...Object.keys(context).length > 0 && { context }
44606
45029
  };
44607
45030
  const { stacks } = await synthesizer.synthesize(synthOpts);
44608
- const task = resolveEcsTaskTarget(target, stacks, await buildEcsImageResolutionContext(target, stacks, options));
45031
+ const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
45032
+ const task = resolveEcsTaskTarget(target, stacks, imageContext);
44609
45033
  logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
45034
+ const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
45035
+ let taskCrossStackDispose;
45036
+ if (options.fromState && taskNeeds.needsCrossStackResolver) {
45037
+ const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
45038
+ const built = await buildCrossStackResolver(consumerRegion, {
45039
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
45040
+ statePrefix: options.statePrefix,
45041
+ ...options.region !== void 0 && { region: options.region },
45042
+ ...options.profile !== void 0 && { profile: options.profile }
45043
+ });
45044
+ if (built) {
45045
+ taskCrossStackDispose = built.dispose;
45046
+ try {
45047
+ await applyCrossStackResolverToTask(task, {
45048
+ resources: imageContext?.stateResources ?? {},
45049
+ ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
45050
+ consumerRegion,
45051
+ crossStackResolver: built.resolver
45052
+ });
45053
+ } finally {
45054
+ taskCrossStackDispose();
45055
+ taskCrossStackDispose = void 0;
45056
+ }
45057
+ }
45058
+ } else if (!options.fromState && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass --from-state to substitute them against deployed cdkd state.");
44610
45059
  sigintHandler = () => {
44611
45060
  sigintCount += 1;
44612
45061
  if (sigintCount >= 2) {
@@ -44894,6 +45343,7 @@ async function localInvokeCommand(target, options) {
44894
45343
  let stateAudit;
44895
45344
  let templateEnv = getTemplateEnv(lambda.resource);
44896
45345
  let stateForRoleHint;
45346
+ let crossStackDispose;
44897
45347
  if (options.fromState) {
44898
45348
  const loaded = await loadStateForStack(lambda.stack.stackName, lambda.stack.region, {
44899
45349
  ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
@@ -44904,16 +45354,38 @@ async function localInvokeCommand(target, options) {
44904
45354
  });
44905
45355
  if (loaded) {
44906
45356
  stateForRoleHint = loaded.state;
44907
- const subContext = { resources: loaded.state.resources };
45357
+ const subContext = {
45358
+ resources: loaded.state.resources,
45359
+ consumerRegion: loaded.region
45360
+ };
44908
45361
  if (envHasIntrinsicValue(templateEnv)) {
44909
45362
  const pseudo = await resolvePseudoParametersForInvoke(lambda.stack.region, options);
44910
45363
  if (pseudo) subContext.pseudoParameters = pseudo;
44911
45364
  }
44912
- const { env, audit } = substituteEnvVarsFromState(templateEnv, subContext);
44913
- templateEnv = env;
44914
- stateAudit = audit;
44915
- for (const key of audit.resolvedKeys) logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
44916
- for (const { key, reason } of audit.unresolved) logger.warn(`--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
45365
+ if (envHasCrossStackIntrinsic(templateEnv)) {
45366
+ const built = await buildCrossStackResolver(loaded.region, {
45367
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
45368
+ statePrefix: options.statePrefix,
45369
+ ...options.region !== void 0 && { region: options.region },
45370
+ ...options.profile !== void 0 && { profile: options.profile }
45371
+ });
45372
+ if (built) {
45373
+ subContext.crossStackResolver = built.resolver;
45374
+ crossStackDispose = built.dispose;
45375
+ }
45376
+ }
45377
+ try {
45378
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
45379
+ templateEnv = env;
45380
+ stateAudit = audit;
45381
+ for (const key of audit.resolvedKeys) logger.debug(`--from-state: substituted env var ${key} from cdkd state`);
45382
+ for (const { key, reason } of audit.unresolved) logger.warn(`--from-state: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
45383
+ } finally {
45384
+ if (crossStackDispose) {
45385
+ crossStackDispose();
45386
+ crossStackDispose = void 0;
45387
+ }
45388
+ }
44917
45389
  }
44918
45390
  }
44919
45391
  const overrides = readEnvOverridesFile(options.envVars);
@@ -45186,6 +45658,29 @@ function envHasIntrinsicValue(templateEnv) {
45186
45658
  return false;
45187
45659
  }
45188
45660
  /**
45661
+ * Returns true when any value in the function's template env map carries
45662
+ * a top-level `Fn::ImportValue` / `Fn::GetStackOutput` intrinsic. Used to
45663
+ * gate the cross-stack resolver construction inside the `--from-state`
45664
+ * flow: literal + same-stack-intrinsic env maps shouldn't pay for the
45665
+ * extra S3 client / index-load cost (issue #454).
45666
+ *
45667
+ * Detection is one level deep — same heuristic CDK 2.x uses for these
45668
+ * intrinsics in practice. Nested cross-stack intrinsics (e.g. an
45669
+ * `Fn::ImportValue` buried inside a `Fn::Join`) are not detected here;
45670
+ * those won't resolve in v1 anyway because the async resolver path
45671
+ * defers to the sync helper for `Fn::Join` / `Fn::Sub` bodies (see
45672
+ * `substituteAgainstStateAsync` docstring).
45673
+ */
45674
+ function envHasCrossStackIntrinsic(templateEnv) {
45675
+ if (!templateEnv) return false;
45676
+ for (const v of Object.values(templateEnv)) {
45677
+ if (!v || typeof v !== "object") continue;
45678
+ const obj = v;
45679
+ if ("Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj) return true;
45680
+ }
45681
+ return false;
45682
+ }
45683
+ /**
45189
45684
  * Build the AWS pseudo-parameter bag for `--from-state` env-var
45190
45685
  * substitution. Issues a single `sts:GetCallerIdentity` for the account
45191
45686
  * id and derives `partition` / `urlSuffix` from the resolved region. Any
@@ -46717,7 +47212,7 @@ function reorderArgs(argv) {
46717
47212
  */
46718
47213
  async function main() {
46719
47214
  const program = new Command();
46720
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.120.0");
47215
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.122.0");
46721
47216
  program.addCommand(createBootstrapCommand());
46722
47217
  program.addCommand(createSynthCommand());
46723
47218
  program.addCommand(createListCommand());