@go-to-k/cdkd 0.232.0 → 0.234.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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { A as withErrorHandling, B as generateResourceName, C as StackHasActiveImportsError, F as getLiveRenderer, H as withSkipPrefix, I as PATTERN_B_NAME_OPTIONS, L as PATTERN_B_NAME_PROPERTIES, M as getLogger, P as runStackBuffered, R as PATTERN_B_RESOURCE_TYPES, S as ResourceUpdateNotSupportedError, U as withStackName, V as generateResourceNameWithFallback, _ as MissingCdkCliError, a as normalizeAwsTagsToCfn, b as ProvisioningError, f as LocalInvokeBuildError$1, i as matchesCdkPath, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as isTerminationProtectionPropagationError, o as resolveExplicitPhysicalId, p as LocalMigrateError, r as CDK_PATH_TAG, s as assertRegionMatch, t as disableInstanceApiTermination, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError } from "./ec2-termination-protection-uvVnteGl.js";
3
3
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-pjPwZz1r.js";
4
- import { $ as resolveSkipPrefix, A as DiffCalculator, B as buildDockerImage, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as AssetManifestLoader, H as getDockerCmd, I as shouldRetainResource, J as synthesisStatusMessage, K as getDockerImageBySourceHash, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as resolveCaptureObservedState, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as runDockerForeground, V as formatDockerLoginError, W as runDockerStreaming, X as getLegacyStateBucketName, Y as getDefaultStateBucketName, Z as resolveApp, _ as green, a as withRetry, at as MIGRATE_TMP_PREFIX, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as AssemblyReader, d as isStatefulRecreateTargetSync, et as resolveStateBucketWithDefault, f as renderStatefulReason, g as gray, h as cyan, i as withResourceDeadline, it as CFN_TEMPLATE_URL_LIMIT, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, m as bold, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as warnDeprecatedNoPrefixCliFlag, o as isRetryableTransientError, ot as findLargeInlineResources, p as formatResourceLine, q as Synthesizer, r as DeployEngine, rt as CFN_TEMPLATE_BODY_LIMIT, s as IMPLICIT_DELETE_DEPENDENCIES, st as uploadCfnTemplate, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as resolveStateBucketWithDefaultAndSource, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveBucketRegion, v as red, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, y as yellow, z as WorkGraph } from "./deploy-engine-BkPZar1R.js";
4
+ import { $ as AssetManifestLoader, A as DiffCalculator, B as buildAssetRedirectMap, C as findActionableSilentDrops, D as refStateLookupFromResource, E as cfnRefValueFromPhysicalId, F as rebuildClientForBucketRegion, G as BOOTSTRAP_MARKER_PREFIX, H as loadPublishableAssetManifest, I as shouldRetainResource, J as buildDockerImage, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as runDockerStreaming, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as getDockerCmd, Y as formatDockerLoginError, Z as runDockerForeground, _ as green, _t as AssemblyReader, a as withRetry, at as resolveApp, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as resolveStateBucketWithDefault, d as isStatefulRecreateTargetSync, dt as warnDeprecatedNoPrefixCliFlag, et as getDockerImageBySourceHash, f as renderStatefulReason, ft as CFN_TEMPLATE_BODY_LIMIT, g as gray, gt as uploadCfnTemplate, h as cyan, ht as findLargeInlineResources, i as withResourceDeadline, it as getLegacyStateBucketName, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveStateBucketWithDefaultAndSource, m as bold, mt as MIGRATE_TMP_PREFIX, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as synthesisStatusMessage, o as isRetryableTransientError, ot as resolveCaptureObservedState, p as formatResourceLine, pt as CFN_TEMPLATE_URL_LIMIT, q as parseBootstrapMarker, r as DeployEngine, rt as getDefaultStateBucketName, s as IMPLICIT_DELETE_DEPENDENCIES, st as resolveSkipPrefix, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as Synthesizer, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveUseCdkBootstrapAssets, v as red, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, y as yellow, yt as resolveBucketRegion, z as WorkGraph } from "./deploy-engine-Dj9mXUQa.js";
5
5
  import { t as ASGProvider } from "./asg-provider-Dd9Xze-T.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -492,6 +492,14 @@ const recreateViaSdkProviderOption = new Option("--recreate-via-sdk-provider <lo
492
492
  */
493
493
  const forceStatefulRecreationOption = new Option("--force-stateful-recreation", "Bypass the stateful-resource guard for --recreate-via-cc-api targets. Required when ANY named target is a stateful type (RDS / DynamoDB / EFS / S3 with data / Logs with retention / Cognito / Secrets / SSM / Glue / ECR / CloudFront / Kinesis / OpenSearch). Destroy + recreate loses ALL data in the resource — no automatic data migration. Triple-opt-in for CI use: --recreate-via-cc-api <id> --force-stateful-recreation --yes.").default(false);
494
494
  const replaceOption = new Option("--replace", "Replace (DELETE + CREATE) a resource whose in-place update is rejected because an immutable property changed and AWS exposes no update API for it (e.g. Lambda LayerVersion content, EFS AccessPoint, ECS TaskDefinition, ApiGatewayV2 immutable fields). Without this flag such a change fails the deploy. Stateful types (RDS / DynamoDB / EFS / S3 with data / Logs with retention / etc.) ALSO require --force-stateful-recreation since the replacement is a data-losing DELETE + CREATE.").default(false);
495
+ /**
496
+ * Issue #1002 PR 2 — per-invocation legacy pin for asset destinations
497
+ * (design §4.2). Shared by `deploy`, `diff`, `import`, and `publish-assets`
498
+ * (every command the redirection / rewrite affects) so the CLI surface stays
499
+ * consistent; the per-app pin is `cdk.json context.cdkd.useCdkBootstrapAssets`
500
+ * (see `resolveUseCdkBootstrapAssets` in config-loader.ts).
501
+ */
502
+ const useCdkBootstrapAssetsOption = new Option("--use-cdk-bootstrap-assets", "Publish assets to the CDK bootstrap bucket/repo named by the asset manifest verbatim, skipping the cdkd asset-storage redirection even when this region is opted in via 'cdkd bootstrap'. For apps deployed via both CloudFormation and cdkd during a migration window. Per-app pin: cdk.json context.cdkd.useCdkBootstrapAssets. Also suppresses the legacy-mode 'cdk gc' notice.").default(false);
495
503
  const deployOptions = [
496
504
  new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
497
505
  new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
@@ -511,6 +519,7 @@ const deployOptions = [
511
519
  recreateViaSdkProviderOption,
512
520
  forceStatefulRecreationOption,
513
521
  replaceOption,
522
+ useCdkBootstrapAssetsOption,
514
523
  ...resourceTimeoutOptions
515
524
  ];
516
525
  /**
@@ -598,310 +607,6 @@ const destroyOptions = [
598
607
  allowUnsupportedTypesOption
599
608
  ];
600
609
 
601
- //#endregion
602
- //#region src/assets/asset-storage.ts
603
- /**
604
- * cdkd-owned asset storage — naming, bootstrap marker, and deploy-time
605
- * asset-mode detection (issue #1002, design at
606
- * docs/design/1002-cdkd-asset-storage.md).
607
- *
608
- * Why this exists: cdkd publishes assets to the CDK bootstrap bucket / ECR
609
- * repo, but `cdk gc` decides "in use" by scanning CloudFormation stack
610
- * templates — cdkd-deployed stacks have no CFn stack, so every cdkd-published
611
- * asset looks isolated and gets deleted. The fix is cdkd-owned asset storage
612
- * created by `cdkd bootstrap` (structurally out of `cdk gc`'s reach because
613
- * gc only discovers storage from the CDK bootstrap stack's outputs).
614
- *
615
- * The upgrade is strictly transparent: deploys behave byte-identically to
616
- * before until the user re-runs `cdkd bootstrap` for a region, which writes a
617
- * per-region marker object into the state bucket. Deploy reads the marker to
618
- * pick the mode; PR 2 of the phasing wires the actual publish redirection +
619
- * template rewrite onto the `cdkd-assets` mode resolved here.
620
- */
621
- /** Marker schema version, bumped if the marker shape ever changes. */
622
- const ASSET_SUPPORT_VERSION = 1;
623
- /**
624
- * S3 key prefix for bootstrap markers in the state bucket. Deliberately
625
- * outside the `cdkd/` state prefix so `state list` key-listing never mistakes
626
- * a marker for a stack, and concurrent bootstraps in two regions cannot race
627
- * last-writer-wins on a shared object (design §12.1).
628
- */
629
- const BOOTSTRAP_MARKER_PREFIX = "cdkd-bootstrap/";
630
- /**
631
- * Name of the cdkd-owned asset bucket for an (account, region) pair.
632
- * Per-region because Lambda requires the code bucket to be in the function's
633
- * region — the same reason CDK's asset bucket is per-region (design §3).
634
- */
635
- function getCdkdAssetBucketName(accountId, region) {
636
- return `cdkd-assets-${accountId}-${region}`;
637
- }
638
- /**
639
- * Name of the cdkd-owned container-asset ECR repository for an
640
- * (account, region) pair. ECR repos are account+region scoped by ARN, so the
641
- * suffix is not strictly needed — the CDK-parallel shape keeps the future
642
- * template rewrite uniform and the names self-describing (design §3).
643
- */
644
- function getCdkdContainerRepoName(accountId, region) {
645
- return `cdkd-container-assets-${accountId}-${region}`;
646
- }
647
- /**
648
- * State-bucket key of the bootstrap marker for a region.
649
- */
650
- function getBootstrapMarkerKey(region) {
651
- return `${BOOTSTRAP_MARKER_PREFIX}${region}.json`;
652
- }
653
- /**
654
- * Parse and validate a bootstrap marker body.
655
- *
656
- * Throws on malformed JSON or missing required fields — a corrupt marker is
657
- * treated like the marker-present-but-resources-missing case (hard error,
658
- * never a silent legacy fallback that would flip-flop stack properties
659
- * between deploys — design §4.1).
660
- */
661
- function parseBootstrapMarker(body, markerKey) {
662
- let parsed;
663
- try {
664
- parsed = JSON.parse(body);
665
- } catch (error) {
666
- throw new CdkdError(`Bootstrap marker '${markerKey}' in the state bucket is not valid JSON. Re-run 'cdkd bootstrap' for this region to rewrite it.`, "INVALID_BOOTSTRAP_MARKER", error);
667
- }
668
- const marker = parsed;
669
- if (typeof marker.assetBucket !== "string" || marker.assetBucket.length === 0 || typeof marker.containerRepo !== "string" || marker.containerRepo.length === 0 || typeof marker.assetSupportVersion !== "number") throw new CdkdError(`Bootstrap marker '${markerKey}' in the state bucket is malformed (missing assetBucket / containerRepo / assetSupportVersion). Re-run 'cdkd bootstrap' for this region to rewrite it.`, "INVALID_BOOTSTRAP_MARKER");
670
- if (marker.assetSupportVersion > 1) throw new CdkdError(`Bootstrap marker '${markerKey}' has assetSupportVersion ${marker.assetSupportVersion}, but this cdkd only understands up to ${1}. Upgrade cdkd to deploy in this region.`, "UNSUPPORTED_BOOTSTRAP_MARKER_VERSION");
671
- return {
672
- assetBucket: marker.assetBucket,
673
- containerRepo: marker.containerRepo,
674
- assetSupportVersion: marker.assetSupportVersion,
675
- createdAt: typeof marker.createdAt === "string" ? marker.createdAt : ""
676
- };
677
- }
678
- /**
679
- * Verify that the asset bucket + ECR repo named by a marker actually exist.
680
- *
681
- * Called when a marker is present: the user opted the region in, so missing
682
- * resources (user deleted them) are a hard error naming the missing resource
683
- * and the `cdkd bootstrap` fix — never a silent fallback to CDK bootstrap
684
- * storage (design §4.1).
685
- *
686
- * Every S3 call passes `ExpectedBucketOwner` so a marker pointing at a
687
- * since-recreated foreign bucket can never leak assets cross-account
688
- * (bucket-squatting defense, design §5).
689
- */
690
- async function verifyAssetStorageExists(marker, accountId, region, opts = {}) {
691
- const rebootstrapHint = `Run 'cdkd bootstrap --region ${region}' to recreate it. cdkd never silently falls back to CDK bootstrap asset storage once a region is opted in.`;
692
- const clientOpts = {
693
- region,
694
- ...opts.profile && { profile: opts.profile }
695
- };
696
- const s3Client = new S3Client(clientOpts);
697
- const ecrClient = new ECRClient(clientOpts);
698
- try {
699
- try {
700
- await s3Client.send(new HeadBucketCommand({
701
- Bucket: marker.assetBucket,
702
- ExpectedBucketOwner: accountId
703
- }));
704
- } catch (error) {
705
- const err = error;
706
- if (err.name === "NotFound" || err.name === "NoSuchBucket") throw new CdkdError(`cdkd asset storage is bootstrapped for region '${region}' but the asset bucket '${marker.assetBucket}' is missing. ${rebootstrapHint}`, "ASSET_STORAGE_MISSING");
707
- if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`Asset bucket '${marker.assetBucket}' exists but is not owned by account ${accountId} (or access is denied). Refusing to use it. ${rebootstrapHint}`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
708
- throw error;
709
- }
710
- try {
711
- await ecrClient.send(new DescribeRepositoriesCommand({ repositoryNames: [marker.containerRepo] }));
712
- } catch (error) {
713
- if (error.name === "RepositoryNotFoundException") throw new CdkdError(`cdkd asset storage is bootstrapped for region '${region}' but the container-asset ECR repository '${marker.containerRepo}' is missing. ${rebootstrapHint}`, "ASSET_STORAGE_MISSING");
714
- throw error;
715
- }
716
- } finally {
717
- s3Client.destroy();
718
- ecrClient.destroy();
719
- }
720
- }
721
- /**
722
- * Create (or adopt, when already owned by this account) the cdkd asset
723
- * bucket + container-asset ECR repo for a region, then write the bootstrap
724
- * marker. Called by `cdkd bootstrap` unless `--no-assets` is passed.
725
- *
726
- * Idempotent like the state-bucket path: existing resources are left as-is
727
- * unless `force` re-applies their configuration. The marker is written LAST,
728
- * only after both resources exist (design §5) — a crash mid-bootstrap leaves
729
- * no marker, so deploys stay in legacy mode.
730
- *
731
- * Bucket-squatting defense: the bucket name is predictable (same weakness as
732
- * CDK's bootstrap bucket), so this refuses to adopt a bucket this account
733
- * does not own, and the `HeadBucket` probe passes `ExpectedBucketOwner`.
734
- */
735
- async function ensureAssetStorage(options) {
736
- const logger = getLogger();
737
- const { s3Client, ecrClient, stateBackend, accountId, region, force } = options;
738
- const assetBucket = getCdkdAssetBucketName(accountId, region);
739
- const containerRepo = getCdkdContainerRepoName(accountId, region);
740
- let bucketExists = false;
741
- try {
742
- await s3Client.send(new HeadBucketCommand({
743
- Bucket: assetBucket,
744
- ExpectedBucketOwner: accountId
745
- }));
746
- bucketExists = true;
747
- logger.info(`Asset bucket ${assetBucket} already exists`);
748
- } catch (error) {
749
- const err = error;
750
- if (err.name === "NotFound" || err.name === "NoSuchBucket") {} else if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`Asset bucket name '${assetBucket}' is already taken by a bucket this account does not own (or access is denied). Refusing to adopt it — resolve the naming conflict before re-running 'cdkd bootstrap'.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
751
- else throw normalizeAwsError(error, {
752
- bucket: assetBucket,
753
- operation: "HeadBucket"
754
- });
755
- }
756
- if (!bucketExists) {
757
- logger.info(`Creating asset bucket: ${assetBucket} in region ${region}`);
758
- try {
759
- await s3Client.send(new CreateBucketCommand({
760
- Bucket: assetBucket,
761
- ...region !== "us-east-1" && { CreateBucketConfiguration: { LocationConstraint: region } }
762
- }));
763
- logger.info(`✓ Created asset bucket: ${assetBucket}`);
764
- } catch (error) {
765
- const err = error;
766
- if (err.name === "BucketAlreadyOwnedByYou") logger.info(`Asset bucket ${assetBucket} already exists`);
767
- else if (err.name === "BucketAlreadyExists") throw new CdkdError(`Asset bucket name '${assetBucket}' is already taken by another AWS account. Refusing to adopt it — resolve the naming conflict before re-running 'cdkd bootstrap'.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
768
- else throw normalizeAwsError(error, {
769
- bucket: assetBucket,
770
- operation: "CreateBucket"
771
- });
772
- }
773
- }
774
- if (!bucketExists || force) {
775
- await s3Client.send(new PutBucketEncryptionCommand({
776
- Bucket: assetBucket,
777
- ExpectedBucketOwner: accountId,
778
- ServerSideEncryptionConfiguration: { Rules: [{
779
- ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
780
- BucketKeyEnabled: true
781
- }] }
782
- }));
783
- await s3Client.send(new PutPublicAccessBlockCommand({
784
- Bucket: assetBucket,
785
- ExpectedBucketOwner: accountId,
786
- PublicAccessBlockConfiguration: {
787
- BlockPublicAcls: true,
788
- BlockPublicPolicy: true,
789
- IgnorePublicAcls: true,
790
- RestrictPublicBuckets: true
791
- }
792
- }));
793
- await s3Client.send(new PutBucketPolicyCommand({
794
- Bucket: assetBucket,
795
- ExpectedBucketOwner: accountId,
796
- Policy: JSON.stringify({
797
- Version: "2012-10-17",
798
- Statement: [{
799
- Sid: "DenyExternalAccess",
800
- Effect: "Deny",
801
- Principal: "*",
802
- Action: "s3:*",
803
- Resource: [`arn:aws:s3:::${assetBucket}`, `arn:aws:s3:::${assetBucket}/*`],
804
- Condition: { StringNotEquals: { "aws:PrincipalAccount": accountId } }
805
- }]
806
- })
807
- }));
808
- logger.info("✓ Configured asset bucket (AES-256 encryption, public access block, deny external access)");
809
- }
810
- let repoExists = false;
811
- try {
812
- await ecrClient.send(new DescribeRepositoriesCommand({ repositoryNames: [containerRepo] }));
813
- repoExists = true;
814
- logger.info(`Container-asset repository ${containerRepo} already exists`);
815
- } catch (error) {
816
- if (error.name !== "RepositoryNotFoundException") throw error;
817
- }
818
- if (!repoExists) {
819
- logger.info(`Creating container-asset ECR repository: ${containerRepo}`);
820
- try {
821
- await ecrClient.send(new CreateRepositoryCommand({
822
- repositoryName: containerRepo,
823
- imageTagMutability: "IMMUTABLE"
824
- }));
825
- logger.info(`✓ Created container-asset ECR repository: ${containerRepo}`);
826
- } catch (error) {
827
- if (error.name === "RepositoryAlreadyExistsException") logger.info(`Container-asset repository ${containerRepo} already exists`);
828
- else throw error;
829
- }
830
- } else if (force) {
831
- await ecrClient.send(new PutImageTagMutabilityCommand({
832
- repositoryName: containerRepo,
833
- imageTagMutability: "IMMUTABLE"
834
- }));
835
- logger.info("✓ Configured container-asset repository (immutable tags)");
836
- }
837
- const marker = {
838
- assetBucket,
839
- containerRepo,
840
- assetSupportVersion: 1,
841
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
842
- };
843
- await stateBackend.putRawObject(getBootstrapMarkerKey(region), JSON.stringify(marker, null, 2));
844
- logger.info(`✓ Wrote bootstrap marker (${getBootstrapMarkerKey(region)})`);
845
- return {
846
- assetBucket,
847
- containerRepo
848
- };
849
- }
850
- /**
851
- * Deploy-time asset-mode resolver. One instance per CLI invocation; results
852
- * are cached per region for the process lifetime (the marker read is one
853
- * GetObject against a bucket every deploy already reads — design §4.1).
854
- *
855
- * In legacy mode, one `logger.info` line per invocation mentions the
856
- * `cdk gc` hazard and the `cdkd bootstrap` opt-in (info, not warn — existing
857
- * users are not doing anything wrong; design §12.2).
858
- */
859
- var AssetModeResolver = class {
860
- logger = getLogger().child("AssetMode");
861
- cache = /* @__PURE__ */ new Map();
862
- legacyNoticeShown = false;
863
- stateBackend;
864
- accountId;
865
- profile;
866
- constructor(stateBackend, accountId, opts = {}) {
867
- this.stateBackend = stateBackend;
868
- this.accountId = accountId;
869
- this.profile = opts.profile;
870
- }
871
- /**
872
- * Resolve the asset mode for a deploy region. Concurrent callers for the
873
- * same region share one in-flight resolution.
874
- */
875
- resolve(region) {
876
- const cached = this.cache.get(region);
877
- if (cached) return cached;
878
- const inFlight = this.doResolve(region).catch((error) => {
879
- this.cache.delete(region);
880
- throw error;
881
- });
882
- this.cache.set(region, inFlight);
883
- return inFlight;
884
- }
885
- async doResolve(region) {
886
- const markerKey = getBootstrapMarkerKey(region);
887
- const body = await this.stateBackend.getRawObject(markerKey);
888
- if (body === null) {
889
- if (!this.legacyNoticeShown) {
890
- this.legacyNoticeShown = true;
891
- this.logger.info("Assets are published to the CDK bootstrap bucket/repo, which 'cdk gc' may garbage-collect (cdkd-deployed stacks have no CloudFormation stack for gc to scan). Run 'cdkd bootstrap' to create cdkd-owned asset storage that 'cdk gc' never touches.");
892
- }
893
- return { mode: "legacy" };
894
- }
895
- const marker = parseBootstrapMarker(body, markerKey);
896
- await verifyAssetStorageExists(marker, this.accountId, region, { ...this.profile && { profile: this.profile } });
897
- this.logger.debug(`cdkd asset storage active for region '${region}': ${marker.assetBucket} / ${marker.containerRepo}`);
898
- return {
899
- mode: "cdkd-assets",
900
- marker
901
- };
902
- }
903
- };
904
-
905
610
  //#endregion
906
611
  //#region src/cli/commands/bootstrap.ts
907
612
  /**
@@ -1795,7 +1500,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1795
1500
  const FLUSH_EVENT_THRESHOLD = 50;
1796
1501
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1797
1502
  function getCdkdVersion() {
1798
- return "0.232.0";
1503
+ return "0.234.0";
1799
1504
  }
1800
1505
  /**
1801
1506
  * Generate a time-sortable unique run id, e.g.
@@ -37119,7 +36824,7 @@ var NestedStackProvider = class {
37119
36824
  this.requireDeployContext(ctx, "create");
37120
36825
  const childTemplatePath = ctx.nestedTemplates[logicalId];
37121
36826
  if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' under parent '${ctx.parentStackName}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default).`);
37122
- const childTemplate = this.readChildTemplate(childTemplatePath);
36827
+ const childTemplate = this.readChildTemplate(childTemplatePath, ctx.assetRedirect);
37123
36828
  const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
37124
36829
  const childRegion = ctx.parentRegion;
37125
36830
  const childParameters = this.extractParameters(properties);
@@ -37138,7 +36843,7 @@ var NestedStackProvider = class {
37138
36843
  this.requireDeployContext(ctx, "update");
37139
36844
  const childTemplatePath = ctx.nestedTemplates[logicalId];
37140
36845
  if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' on update.`);
37141
- const childTemplate = this.readChildTemplate(childTemplatePath);
36846
+ const childTemplate = this.readChildTemplate(childTemplatePath, ctx.assetRedirect);
37142
36847
  const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
37143
36848
  const childRegion = ctx.parentRegion;
37144
36849
  const childParameters = this.extractParameters(properties);
@@ -37233,18 +36938,24 @@ var NestedStackProvider = class {
37233
36938
  else throw new Error(`NestedStackProvider: child Parameter '${k}' resolved to a non-scalar value (type=${v === null ? "null" : typeof v}). Parameters must be scalars (string / number / boolean) by the time they reach the provider — an unresolved intrinsic here means IntrinsicFunctionResolver upstream did not handle the value, which is a bug. Surface the unresolved input rather than silently coercing to '[object Object]'.`);
37234
36939
  return result;
37235
36940
  }
37236
- readChildTemplate(templatePath) {
36941
+ readChildTemplate(templatePath, assetRedirect) {
37237
36942
  let raw;
37238
36943
  try {
37239
36944
  raw = fs.readFileSync(templatePath, "utf-8");
37240
36945
  } catch (err) {
37241
36946
  throw new Error(`Failed to read nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
37242
36947
  }
36948
+ let template;
37243
36949
  try {
37244
- return JSON.parse(raw);
36950
+ template = JSON.parse(raw);
37245
36951
  } catch (err) {
37246
36952
  throw new Error(`Failed to parse nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
37247
36953
  }
36954
+ if (assetRedirect) {
36955
+ const n = rewriteTemplateAssetReferences(template, assetRedirect);
36956
+ if (n > 0) this.logger.debug(`Rewrote ${n} asset reference(s) to cdkd asset storage in nested template ${templatePath}`);
36957
+ }
36958
+ return template;
37248
36959
  }
37249
36960
  indexGrandchildTemplates(childTemplate, childTemplatePath) {
37250
36961
  const dir = path.dirname(childTemplatePath);
@@ -37994,7 +37705,11 @@ async function deployCommand(stacks, options) {
37994
37705
  const accountId = (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
37995
37706
  stsClient.destroy();
37996
37707
  const assetPublisher = new AssetPublisher();
37997
- const assetModeResolver = new AssetModeResolver(preflightStateBackend, accountId, { ...options.profile && { profile: options.profile } });
37708
+ const useCdkBootstrapAssets = resolveUseCdkBootstrapAssets(options.useCdkBootstrapAssets);
37709
+ const assetModeResolver = new AssetModeResolver(preflightStateBackend, accountId, {
37710
+ ...options.profile && { profile: options.profile },
37711
+ ...useCdkBootstrapAssets && { useCdkBootstrapAssets: true }
37712
+ });
37998
37713
  const stateConfig = {
37999
37714
  bucket: stateBucket,
38000
37715
  prefix: options.statePrefix
@@ -38008,27 +37723,36 @@ async function deployCommand(stacks, options) {
38008
37723
  };
38009
37724
  const workGraph = new WorkGraph();
38010
37725
  const stackMap = new Map(targetStacks.map((s) => [s.stackName, s]));
37726
+ const stackRedirects = /* @__PURE__ */ new Map();
38011
37727
  for (const stack of targetStacks) {
38012
37728
  const stackNodeId = `stack:${stack.stackName}`;
38013
37729
  const stackDeps = /* @__PURE__ */ new Set();
38014
- if (!options.skipAssets && stack.assetManifestPath) {
37730
+ if (stack.assetManifestPath) {
38015
37731
  const assetRegion = stack.region || baseRegion;
38016
- let nodeIds = [];
38017
- try {
38018
- nodeIds = assetPublisher.addAssetsToGraph(workGraph, stack.assetManifestPath, {
38019
- accountId,
38020
- region: assetRegion,
38021
- ...options.profile && { profile: options.profile },
38022
- nodePrefix: `${stack.stackName}:`
38023
- });
38024
- } catch (error) {
38025
- if (error.code !== "ENOENT") throw error;
38026
- }
38027
- if (nodeIds.length > 0) {
37732
+ const manifest = loadPublishableAssetManifest(stack.assetManifestPath);
37733
+ if (manifest) {
38028
37734
  const assetMode = await assetModeResolver.resolve(assetRegion);
38029
37735
  logger.debug(`Asset mode for region ${assetRegion}: ${assetMode.mode} (stack ${stack.stackName})`);
37736
+ let redirect;
37737
+ if (assetMode.mode === "cdkd-assets") {
37738
+ redirect = buildAssetRedirectMap(manifest, assetMode.marker, accountId, assetRegion);
37739
+ if (redirect.entries.length > 0) {
37740
+ stackRedirects.set(stack.stackName, redirect);
37741
+ const rewritten = rewriteTemplateAssetReferences(stack.template, redirect);
37742
+ logger.debug(`Rewrote ${rewritten} asset reference(s) to cdkd asset storage (${assetMode.marker.assetBucket} / ${assetMode.marker.containerRepo}) in template of stack ${stack.stackName}`);
37743
+ } else redirect = void 0;
37744
+ }
37745
+ if (!options.skipAssets) {
37746
+ const nodeIds = assetPublisher.addAssetsToGraph(workGraph, stack.assetManifestPath, {
37747
+ accountId,
37748
+ region: assetRegion,
37749
+ ...options.profile && { profile: options.profile },
37750
+ nodePrefix: `${stack.stackName}:`,
37751
+ ...redirect && { redirect }
37752
+ });
37753
+ for (const id of nodeIds) stackDeps.add(id);
37754
+ }
38030
37755
  }
38031
- for (const id of nodeIds) stackDeps.add(id);
38032
37756
  }
38033
37757
  for (const depName of effectiveStackDeps(stack.stackName, stack.dependencyNames)) if (stackMap.has(depName)) stackDeps.add(`stack:${depName}`);
38034
37758
  workGraph.addNode({
@@ -38127,10 +37851,12 @@ async function deployCommand(stacks, options) {
38127
37851
  })) return;
38128
37852
  }
38129
37853
  }
37854
+ const assetRedirect = stackRedirects.get(stackInfo.stackName);
38130
37855
  const deployEngineOptions = {
38131
37856
  concurrency: options.concurrency,
38132
37857
  dryRun: options.dryRun,
38133
37858
  noRollback: !options.rollback,
37859
+ ...assetRedirect && { assetRedirect },
38134
37860
  ...eventRecorder && { eventRecorder },
38135
37861
  ...recreateViaCcApiTargets && recreateViaCcApiTargets.size > 0 && { recreateViaCcApiTargets },
38136
37862
  ...recreateViaSdkProviderTargets && recreateViaSdkProviderTargets.size > 0 && { recreateViaSdkProviderTargets },
@@ -38156,7 +37882,8 @@ async function deployCommand(stacks, options) {
38156
37882
  nestedTemplates: stackInfo.nestedTemplates ?? {},
38157
37883
  dagBuilder,
38158
37884
  diffCalculator,
38159
- options: deployEngineOptions
37885
+ options: deployEngineOptions,
37886
+ ...assetRedirect && { assetRedirect }
38160
37887
  }, () => stackDeployEngine.deploy(stackInfo.stackName, stackInfo.template));
38161
37888
  logger.info(`\n${bold("Deployment Summary:")}`);
38162
37889
  logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
@@ -39194,7 +38921,7 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
39194
38921
  * fail at deploy time.
39195
38922
  */
39196
38923
  async function buildDiffTree(args) {
39197
- const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parameters } = args;
38924
+ const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parameters, assetRedirect } = args;
39198
38925
  const state = await loadStateOrEmpty(stackName, region, stateBackend);
39199
38926
  const node = {
39200
38927
  stackName,
@@ -39213,6 +38940,7 @@ async function buildDiffTree(args) {
39213
38940
  if (!childTemplatePath) throw new Error(`Nested template file not found for ${NESTED_STACK_RESOURCE_TYPE} '${logicalId}' under stack '${stackName}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default), then re-run synth.`);
39214
38941
  const childStackName = `${stackName}~${logicalId}`;
39215
38942
  const childTemplate = readNestedTemplate(childTemplatePath);
38943
+ if (assetRedirect) rewriteTemplateAssetReferences(childTemplate, assetRedirect);
39216
38944
  const grandchildTemplates = indexNestedChildTemplates(childTemplate, childTemplatePath);
39217
38945
  const childParameters = await resolveChildStackParameters(resource, template, state, region, stackName, stateBackend, parameters);
39218
38946
  node.children.push(await buildDiffTree({
@@ -39224,7 +38952,8 @@ async function buildDiffTree(args) {
39224
38952
  recursive: true,
39225
38953
  stateBackend,
39226
38954
  diffCalculator,
39227
- parameters: childParameters
38955
+ parameters: childParameters,
38956
+ ...assetRedirect && { assetRedirect }
39228
38957
  }));
39229
38958
  }
39230
38959
  for (const [logicalId, resource] of Object.entries(state.resources)) {
@@ -39534,10 +39263,22 @@ async function diffCommand(stacks, options) {
39534
39263
  });
39535
39264
  const diffCalculator = new DiffCalculator();
39536
39265
  const recursive = options.recursive ?? false;
39266
+ const resolveAssetRedirect = createAssetRedirectResolver({
39267
+ stateBackend,
39268
+ stsRegion: region,
39269
+ ...options.profile && { profile: options.profile },
39270
+ useCdkBootstrapAssets: resolveUseCdkBootstrapAssets(options.useCdkBootstrapAssets),
39271
+ suppressLegacyNotice: true
39272
+ });
39537
39273
  const trees = [];
39538
39274
  for (const stackInfo of targetStacks) {
39539
39275
  logger.info(`\nCalculating diff for stack: ${stackInfo.stackName}`);
39540
39276
  const stackRegion = stackInfo.region || region;
39277
+ const assetRedirect = await resolveAssetRedirect(stackInfo.assetManifestPath, stackRegion);
39278
+ if (assetRedirect) {
39279
+ const rewritten = rewriteTemplateAssetReferences(stackInfo.template, assetRedirect);
39280
+ logger.debug(`Rewrote ${rewritten} asset reference(s) to cdkd asset storage in template of stack ${stackInfo.stackName}`);
39281
+ }
39541
39282
  trees.push(await buildDiffTree({
39542
39283
  stackName: stackInfo.stackName,
39543
39284
  displayName: stackInfo.stackName,
@@ -39546,7 +39287,8 @@ async function diffCommand(stacks, options) {
39546
39287
  nestedTemplates: stackInfo.nestedTemplates ?? {},
39547
39288
  recursive,
39548
39289
  stateBackend,
39549
- diffCalculator
39290
+ diffCalculator,
39291
+ ...assetRedirect && { assetRedirect }
39550
39292
  }));
39551
39293
  }
39552
39294
  if (options.json) process.stdout.write(`${JSON.stringify(trees.map(diffTreeToJson), null, 2)}\n`);
@@ -39566,7 +39308,7 @@ async function diffCommand(stacks, options) {
39566
39308
  * Create diff command
39567
39309
  */
39568
39310
  function createDiffCommand() {
39569
- const cmd = new Command("diff").description("Show difference between current state and desired state").argument("[stacks...]", "Stack name(s) to diff. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*').").option("--all", "Diff all stacks", false).option("--recursive", "Recurse into each AWS::CloudFormation::Stack row and diff every nested-stack child against its own deployed state (DFS order). Default is non-recursive, matching cdk diff.", false).option("--fail", "Exit with code 1 when any change is detected (matches cdk diff --fail). With --recursive, considers the whole nested-stack tree.", false).option("--json", "Output the diff as JSON (nested tree shape when combined with --recursive)", false).action(withErrorHandling(diffCommand));
39311
+ const cmd = new Command("diff").description("Show difference between current state and desired state").argument("[stacks...]", "Stack name(s) to diff. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*').").option("--all", "Diff all stacks", false).option("--recursive", "Recurse into each AWS::CloudFormation::Stack row and diff every nested-stack child against its own deployed state (DFS order). Default is non-recursive, matching cdk diff.", false).option("--fail", "Exit with code 1 when any change is detected (matches cdk diff --fail). With --recursive, considers the whole nested-stack tree.", false).option("--json", "Output the diff as JSON (nested tree shape when combined with --recursive)", false).addOption(useCdkBootstrapAssetsOption).action(withErrorHandling(diffCommand));
39570
39312
  [
39571
39313
  ...commonOptions,
39572
39314
  ...appOptions,
@@ -41859,36 +41601,45 @@ async function publishAssetsCommand(stacks, options) {
41859
41601
  const stsClient = new STSClient({ region: baseRegion });
41860
41602
  const accountId = (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
41861
41603
  stsClient.destroy();
41604
+ const useCdkBootstrapAssets = resolveUseCdkBootstrapAssets(options.useCdkBootstrapAssets);
41605
+ let markerAccess;
41606
+ const getModeResolver = async () => {
41607
+ if (markerAccess !== void 0) return markerAccess?.resolver ?? null;
41608
+ let bucket;
41609
+ try {
41610
+ bucket = await resolveStateBucketWithDefault(options.stateBucket, baseRegion);
41611
+ } catch {
41612
+ logger.info("No cdkd state bucket found — publishing to the asset-manifest destinations (CDK bootstrap storage) verbatim. Run 'cdkd bootstrap' to opt into cdkd-owned asset storage that 'cdk gc' never touches.");
41613
+ markerAccess = null;
41614
+ return null;
41615
+ }
41616
+ const markerClients = new AwsClients({
41617
+ region: baseRegion,
41618
+ ...options.profile && { profile: options.profile }
41619
+ });
41620
+ const resolver = new AssetModeResolver(new S3StateBackend(markerClients.s3, {
41621
+ bucket,
41622
+ prefix: options.statePrefix
41623
+ }, {
41624
+ region: baseRegion,
41625
+ ...options.profile && { profile: options.profile }
41626
+ }), accountId, { ...options.profile && { profile: options.profile } });
41627
+ markerAccess = {
41628
+ resolver,
41629
+ dispose: () => markerClients.destroy()
41630
+ };
41631
+ return resolver;
41632
+ };
41862
41633
  const assetPublisher = new AssetPublisher();
41863
41634
  const results = [];
41864
- for (const stack of targetStacks) {
41865
- const startedAt = Date.now();
41866
- let assetCount = 0;
41867
- let error;
41868
- try {
41869
- if (!stack.assetManifestPath) {
41870
- logger.debug(`Stack ${stack.stackName} has no asset manifest; nothing to publish`);
41871
- results.push({
41872
- stackName: stack.stackName,
41873
- displayName: stack.displayName,
41874
- assetCount: 0,
41875
- durationMs: Date.now() - startedAt
41876
- });
41877
- continue;
41878
- }
41879
- logger.info(`\nPublishing assets for stack: ${describeStack(stack)}`);
41880
- const workGraph = new WorkGraph();
41881
- let nodeIds = [];
41635
+ try {
41636
+ for (const stack of targetStacks) {
41637
+ const startedAt = Date.now();
41638
+ let assetCount = 0;
41639
+ let error;
41882
41640
  try {
41883
- nodeIds = assetPublisher.addAssetsToGraph(workGraph, stack.assetManifestPath, {
41884
- accountId,
41885
- region: stack.region || baseRegion,
41886
- ...options.profile && { profile: options.profile },
41887
- nodePrefix: `${stack.stackName}:`
41888
- });
41889
- } catch (err) {
41890
- if (err.code === "ENOENT") {
41891
- logger.debug(`Asset manifest not found for ${stack.stackName} (${stack.assetManifestPath}); skipping`);
41641
+ if (!stack.assetManifestPath) {
41642
+ logger.debug(`Stack ${stack.stackName} has no asset manifest; nothing to publish`);
41892
41643
  results.push({
41893
41644
  stackName: stack.stackName,
41894
41645
  displayName: stack.displayName,
@@ -41897,36 +41648,76 @@ async function publishAssetsCommand(stacks, options) {
41897
41648
  });
41898
41649
  continue;
41899
41650
  }
41900
- throw err;
41901
- }
41902
- assetCount = nodeIds.filter((id) => id.startsWith("asset-publish:")).length;
41903
- if (assetCount === 0) {
41904
- logger.info(" (no assets to publish)");
41905
- results.push({
41906
- stackName: stack.stackName,
41907
- displayName: stack.displayName,
41908
- assetCount: 0,
41909
- durationMs: Date.now() - startedAt
41910
- });
41911
- continue;
41651
+ logger.info(`\nPublishing assets for stack: ${describeStack(stack)}`);
41652
+ let redirect;
41653
+ if (!useCdkBootstrapAssets) {
41654
+ const manifest = loadPublishableAssetManifest(stack.assetManifestPath);
41655
+ if (manifest) {
41656
+ const modeResolver = await getModeResolver();
41657
+ if (modeResolver) {
41658
+ const stackRegion = stack.region || baseRegion;
41659
+ const assetMode = await modeResolver.resolve(stackRegion);
41660
+ if (assetMode.mode === "cdkd-assets") {
41661
+ const map = buildAssetRedirectMap(manifest, assetMode.marker, accountId, stackRegion);
41662
+ if (map.entries.length > 0) redirect = map;
41663
+ }
41664
+ }
41665
+ }
41666
+ }
41667
+ const workGraph = new WorkGraph();
41668
+ let nodeIds = [];
41669
+ try {
41670
+ nodeIds = assetPublisher.addAssetsToGraph(workGraph, stack.assetManifestPath, {
41671
+ accountId,
41672
+ region: stack.region || baseRegion,
41673
+ ...options.profile && { profile: options.profile },
41674
+ nodePrefix: `${stack.stackName}:`,
41675
+ ...redirect && { redirect }
41676
+ });
41677
+ } catch (err) {
41678
+ if (err.code === "ENOENT") {
41679
+ logger.debug(`Asset manifest not found for ${stack.stackName} (${stack.assetManifestPath}); skipping`);
41680
+ results.push({
41681
+ stackName: stack.stackName,
41682
+ displayName: stack.displayName,
41683
+ assetCount: 0,
41684
+ durationMs: Date.now() - startedAt
41685
+ });
41686
+ continue;
41687
+ }
41688
+ throw err;
41689
+ }
41690
+ assetCount = nodeIds.filter((id) => id.startsWith("asset-publish:")).length;
41691
+ if (assetCount === 0) {
41692
+ logger.info(" (no assets to publish)");
41693
+ results.push({
41694
+ stackName: stack.stackName,
41695
+ displayName: stack.displayName,
41696
+ assetCount: 0,
41697
+ durationMs: Date.now() - startedAt
41698
+ });
41699
+ continue;
41700
+ }
41701
+ await workGraph.execute({
41702
+ "asset-build": options.imageBuildConcurrency,
41703
+ "asset-publish": options.assetPublishConcurrency,
41704
+ stack: 0
41705
+ }, (node) => assetPublisher.executeNode(node));
41706
+ logger.info(` ✓ Published ${assetCount} asset(s) in ${((Date.now() - startedAt) / 1e3).toFixed(2)}s`);
41707
+ } catch (err) {
41708
+ error = err instanceof Error ? err : new Error(String(err));
41709
+ logger.error(` ✗ ${stack.stackName}: ${error.message}`);
41912
41710
  }
41913
- await workGraph.execute({
41914
- "asset-build": options.imageBuildConcurrency,
41915
- "asset-publish": options.assetPublishConcurrency,
41916
- stack: 0
41917
- }, (node) => assetPublisher.executeNode(node));
41918
- logger.info(` ✓ Published ${assetCount} asset(s) in ${((Date.now() - startedAt) / 1e3).toFixed(2)}s`);
41919
- } catch (err) {
41920
- error = err instanceof Error ? err : new Error(String(err));
41921
- logger.error(` ✗ ${stack.stackName}: ${error.message}`);
41922
- }
41923
- results.push({
41924
- stackName: stack.stackName,
41925
- displayName: stack.displayName,
41926
- assetCount,
41927
- durationMs: Date.now() - startedAt,
41928
- ...error && { error }
41929
- });
41711
+ results.push({
41712
+ stackName: stack.stackName,
41713
+ displayName: stack.displayName,
41714
+ assetCount,
41715
+ durationMs: Date.now() - startedAt,
41716
+ ...error && { error }
41717
+ });
41718
+ }
41719
+ } finally {
41720
+ markerAccess?.dispose();
41930
41721
  }
41931
41722
  const failed = results.filter((r) => r.error);
41932
41723
  const totalAssets = results.reduce((sum, r) => sum + r.assetCount, 0);
@@ -41949,11 +41740,12 @@ async function publishAssetsCommand(stacks, options) {
41949
41740
  * `cdkd deploy --no-asset-publish` (which doesn't exist).
41950
41741
  */
41951
41742
  function createPublishAssetsCommand() {
41952
- const cmd = new Command("publish-assets").description("Synthesize the CDK app and publish assets to S3/ECR for the selected stack(s) without deploying").argument("[stacks...]", "Stack name(s) to publish assets for. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards.").option("--all", "Publish assets for all stacks", false).addOption(new Option("--asset-publish-concurrency <number>", "Maximum concurrent asset publish operations").default(8).argParser((value) => parseInt(value, 10))).addOption(new Option("--image-build-concurrency <number>", "Maximum concurrent Docker image builds").default(4).argParser((value) => parseInt(value, 10))).action(withErrorHandling(publishAssetsCommand));
41743
+ const cmd = new Command("publish-assets").description("Synthesize the CDK app and publish assets to S3/ECR for the selected stack(s) without deploying").argument("[stacks...]", "Stack name(s) to publish assets for. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards.").option("--all", "Publish assets for all stacks", false).addOption(new Option("--asset-publish-concurrency <number>", "Maximum concurrent asset publish operations").default(8).argParser((value) => parseInt(value, 10))).addOption(new Option("--image-build-concurrency <number>", "Maximum concurrent Docker image builds").default(4).argParser((value) => parseInt(value, 10))).addOption(useCdkBootstrapAssetsOption).action(withErrorHandling(publishAssetsCommand));
41953
41744
  [
41954
41745
  ...commonOptions,
41955
41746
  ...appOptions,
41956
- ...contextOptions
41747
+ ...contextOptions,
41748
+ ...stateOptions
41957
41749
  ].forEach((opt) => cmd.addOption(opt));
41958
41750
  cmd.addOption(new Option("--stack <name>", "Stack name to publish (alternative to positional)"));
41959
41751
  cmd.addOption(deprecatedRegionOption);
@@ -45977,6 +45769,17 @@ async function importCommand(stackArg, options) {
45977
45769
  else throw new Error(`Multiple stacks found: ${result.stacks.map((s) => s.stackName).join(", ")}. Specify the stack name as a positional argument.`);
45978
45770
  const targetRegion = stackInfo.region || region;
45979
45771
  logger.info(`Target stack: ${stackInfo.stackName} (${targetRegion})`);
45772
+ const assetRedirect = await createAssetRedirectResolver({
45773
+ stateBackend,
45774
+ stsRegion: region,
45775
+ ...options.profile && { profile: options.profile },
45776
+ useCdkBootstrapAssets: resolveUseCdkBootstrapAssets(options.useCdkBootstrapAssets),
45777
+ suppressLegacyNotice: true
45778
+ })(stackInfo.assetManifestPath, targetRegion);
45779
+ if (assetRedirect) {
45780
+ const rewritten = rewriteTemplateAssetReferences(stackInfo.template, assetRedirect);
45781
+ logger.debug(`Rewrote ${rewritten} asset reference(s) to cdkd asset storage in template of stack ${stackInfo.stackName}`);
45782
+ }
45980
45783
  const overrides = parseResourceOverrides(options.resource, options.resourceMapping, options.resourceMappingInline);
45981
45784
  if (overrides.size > 0) logger.debug(`User-supplied physical IDs: ${[...overrides.keys()].join(", ")}`);
45982
45785
  const migrationCfnStackName = options.migrateFromCloudformation ? typeof options.migrateFromCloudformation === "string" && options.migrateFromCloudformation.length > 0 ? options.migrateFromCloudformation : stackInfo.stackName : void 0;
@@ -46113,7 +45916,8 @@ async function importCommand(stackArg, options) {
46113
45916
  templateParser,
46114
45917
  lockOwner: owner,
46115
45918
  accountId: accountIdForNestedSynth,
46116
- logger
45919
+ logger,
45920
+ assetRedirect
46117
45921
  });
46118
45922
  if (migrationCfnStackName) {
46119
45923
  const orphaned = resources.length - importedRows.length;
@@ -46528,7 +46332,7 @@ async function confirmPrompt(prompt) {
46528
46332
  * default behavior, now opt-in.
46529
46333
  */
46530
46334
  function createImportCommand() {
46531
- const cmd = new Command("import").description("Adopt already-deployed AWS resources into cdkd state. Reads the CDK app to find logical IDs, resource types, and dependencies. With no flags, imports every resource via the aws:cdk:path tag. With --resource / --resource-mapping, only the listed resources are imported (CDK CLI parity); pass --auto to also tag-import the rest.").argument("[stack]", "Stack to import. Optional when the synthesized app contains exactly one stack.").option("--resource <id=physical>", "Explicit physical-id override for one logical ID. Repeatable. When at least one --resource is given, only listed resources are imported (CDK CLI parity). Pass --auto to also tag-import everything else.", collectMultiple, []).option("--resource-mapping <file>", "Path to a JSON file of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping` compatible). Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping-inline.").option("--resource-mapping-inline <json>", "Inline JSON object of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping-inline` compatible). Same shape as --resource-mapping but supplied as a string — useful for non-TTY CI scripts that do not want a separate file. Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping.").option("--record-resource-mapping <file>", "After cdkd resolves every logical ID (via --resource / --resource-mapping / tag-based auto-lookup), write the resulting {logicalId: physicalId} map to <file> as JSON. Useful in auto / hybrid mode for capturing the tag-resolved mapping and feeding it back as --resource-mapping in non-interactive CI re-runs. Written before the confirmation prompt (so the user can review the file before saying \"yes\") and even when the user says \"no\". Mirrors `cdk import --record-resource-mapping`.").option("--auto", "Hybrid mode: when explicit overrides are supplied, ALSO tag-import every other resource in the template. Without this flag, --resource / --resource-mapping behave as a whitelist (CDK CLI parity).", false).option("--dry-run", "Show planned imports without writing state", false).option("--force", "Confirm a destructive write to existing state. Required for auto / whole-stack import when state already exists (rebuilds the entire resource map). Also required in selective mode if a listed override would overwrite a resource already in state. Not needed for a pure selective merge (adding new resources without touching unlisted entries).", false).option("--migrate-from-cloudformation [cfn-stack-name]", "After cdkd state is written, retire the named CloudFormation stack (deletes the CFn stack record; AWS resources are NOT deleted): inject DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource via UpdateStack, then DeleteStack. cdkd takes over management. Pass without a value to use the cdkd stack name as the CFn stack name (the typical case for a CDK app that was previously deployed via `cdk deploy`); pass an explicit value when the CFn stack name differs.").action(withErrorHandling(importCommand));
46335
+ const cmd = new Command("import").description("Adopt already-deployed AWS resources into cdkd state. Reads the CDK app to find logical IDs, resource types, and dependencies. With no flags, imports every resource via the aws:cdk:path tag. With --resource / --resource-mapping, only the listed resources are imported (CDK CLI parity); pass --auto to also tag-import the rest.").argument("[stack]", "Stack to import. Optional when the synthesized app contains exactly one stack.").option("--resource <id=physical>", "Explicit physical-id override for one logical ID. Repeatable. When at least one --resource is given, only listed resources are imported (CDK CLI parity). Pass --auto to also tag-import everything else.", collectMultiple, []).option("--resource-mapping <file>", "Path to a JSON file of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping` compatible). Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping-inline.").option("--resource-mapping-inline <json>", "Inline JSON object of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping-inline` compatible). Same shape as --resource-mapping but supplied as a string — useful for non-TTY CI scripts that do not want a separate file. Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping.").option("--record-resource-mapping <file>", "After cdkd resolves every logical ID (via --resource / --resource-mapping / tag-based auto-lookup), write the resulting {logicalId: physicalId} map to <file> as JSON. Useful in auto / hybrid mode for capturing the tag-resolved mapping and feeding it back as --resource-mapping in non-interactive CI re-runs. Written before the confirmation prompt (so the user can review the file before saying \"yes\") and even when the user says \"no\". Mirrors `cdk import --record-resource-mapping`.").option("--auto", "Hybrid mode: when explicit overrides are supplied, ALSO tag-import every other resource in the template. Without this flag, --resource / --resource-mapping behave as a whitelist (CDK CLI parity).", false).option("--dry-run", "Show planned imports without writing state", false).option("--force", "Confirm a destructive write to existing state. Required for auto / whole-stack import when state already exists (rebuilds the entire resource map). Also required in selective mode if a listed override would overwrite a resource already in state. Not needed for a pure selective merge (adding new resources without touching unlisted entries).", false).option("--migrate-from-cloudformation [cfn-stack-name]", "After cdkd state is written, retire the named CloudFormation stack (deletes the CFn stack record; AWS resources are NOT deleted): inject DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource via UpdateStack, then DeleteStack. cdkd takes over management. Pass without a value to use the cdkd stack name as the CFn stack name (the typical case for a CDK app that was previously deployed via `cdk deploy`); pass an explicit value when the CFn stack name differs.").addOption(useCdkBootstrapAssetsOption).action(withErrorHandling(importCommand));
46532
46336
  [
46533
46337
  ...commonOptions,
46534
46338
  ...appOptions,
@@ -46666,7 +46470,7 @@ function validateNestedStackShape(template, tree, parentStackName, nestedTemplat
46666
46470
  * is verified by unit test.
46667
46471
  */
46668
46472
  async function importNestedStackChildrenRecursive(args) {
46669
- const { parentStackName, parentRegion, parentNestedTemplates, parentTree, stateBackend, lockManager, providerRegistry, templateParser, lockOwner, accountId, logger } = args;
46473
+ const { parentStackName, parentRegion, parentNestedTemplates, parentTree, stateBackend, lockManager, providerRegistry, templateParser, lockOwner, accountId, logger, assetRedirect } = args;
46670
46474
  for (const [childLogicalId, childTreeNode] of parentTree.nested) {
46671
46475
  const childStackName = `${parentStackName}~${childLogicalId}`;
46672
46476
  const childRegion = parentRegion;
@@ -46674,6 +46478,7 @@ async function importNestedStackChildrenRecursive(args) {
46674
46478
  if (!childTemplatePath) throw new Error(`cdkd import --migrate-from-cloudformation: missing nested-template path for '${childLogicalId}' under parent '${parentStackName}' — validateNestedStackShape should have rejected this; please file a bug.`);
46675
46479
  logger.info(`Adopting nested stack '${childLogicalId}' as cdkd stack '${childStackName}' (${childRegion})...`);
46676
46480
  const childTemplate = readNestedChildTemplate(childTemplatePath, childLogicalId);
46481
+ if (assetRedirect) rewriteTemplateAssetReferences(childTemplate, assetRedirect);
46677
46482
  await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import");
46678
46483
  try {
46679
46484
  const childResources = collectImportableResources(childTemplate);
@@ -46725,7 +46530,8 @@ async function importNestedStackChildrenRecursive(args) {
46725
46530
  templateParser,
46726
46531
  lockOwner,
46727
46532
  accountId,
46728
- logger
46533
+ logger,
46534
+ assetRedirect
46729
46535
  });
46730
46536
  } finally {
46731
46537
  await lockManager.releaseLock(childStackName, childRegion).catch((err) => {
@@ -48070,6 +47876,28 @@ function buildSubstitutionContextFromImageContext(context) {
48070
47876
  return subContext;
48071
47877
  }
48072
47878
  /**
47879
+ * A container-image URI that points at a CDK-bootstrap OR cdkd-owned assets
47880
+ * ECR repository:
47881
+ *
47882
+ * - `cdk-<qualifier>-container-assets-<acct>-<region>` — the CDK bootstrap
47883
+ * repo (default qualifier `hnb659fds`, or a custom `cdk bootstrap
47884
+ * --qualifier` value);
47885
+ * - `cdkd-container-assets-<acct>-<region>` — the repo `cdkd bootstrap`
47886
+ * creates and `cdkd deploy` publishes into once a bootstrap marker exists
47887
+ * (issue #1002; a migrated stack's template / `--from-state` state carries
47888
+ * this shape).
47889
+ *
47890
+ * Either is a CDK asset image the ECS runner resolves back to the on-disk
47891
+ * `cdk.out` build, so both must classify as `kind: 'cdk-asset'`. The `cdk-`
47892
+ * qualifier is generalized to `[a-z0-9]+` so a custom-qualifier bootstrap is
47893
+ * matched too (previously only the hardcoded `hnb659fds` default was).
47894
+ */
47895
+ const CDK_ASSET_IMAGE_REPO_RE = /(?:cdk-[a-z0-9]+|cdkd)-container-assets-/;
47896
+ /** True when `uri` embeds a CDK-bootstrap or cdkd-owned container-assets repo. */
47897
+ function isCdkAssetImageUri(uri) {
47898
+ return CDK_ASSET_IMAGE_REPO_RE.test(uri);
47899
+ }
47900
+ /**
48073
47901
  * Parse the `Image` field of an ECS container definition.
48074
47902
  *
48075
47903
  * Three shapes:
@@ -48109,7 +47937,7 @@ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stac
48109
47937
  if (joinResolved.kind === "unsupported-join") throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an unsupported Fn::Join Image shape: ${joinResolved.reason}. cdkd local run-task recognizes the canonical CDK 2.x ContainerImage.fromEcrRepository Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`);
48110
47938
  const flat = extractImageString(raw);
48111
47939
  if (!flat) throw new EcsTaskResolutionError(`Container '${containerName}' in task '${taskLogicalId}' has an unparseable Image property. cdkd local run-task v1 supports flat string images, single-key Fn::Sub bodies, and CDK-asset Image references.`);
48112
- if (flat.includes("cdk-hnb659fds-container-assets-")) {
47940
+ if (isCdkAssetImageUri(flat)) {
48113
47941
  const hashMatch = /:([a-f0-9]{8,})$/.exec(flat);
48114
47942
  const out = { kind: "cdk-asset" };
48115
47943
  if (hashMatch) out.assetHash = hashMatch[1];
@@ -48157,7 +47985,7 @@ function findUnresolvedEcrRepositoryRef(substituted, resources) {
48157
47985
  * `Fn::GetAtt` path can share the regex-match branches.
48158
47986
  */
48159
47987
  function classifyResolvedImage(uri) {
48160
- if (uri.includes("cdk-hnb659fds-container-assets-")) {
47988
+ if (isCdkAssetImageUri(uri)) {
48161
47989
  const hashMatch = /:([a-f0-9]{8,})$/.exec(uri);
48162
47990
  const out = { kind: "cdk-asset" };
48163
47991
  if (hashMatch) out.assetHash = hashMatch[1];
@@ -56183,7 +56011,7 @@ function reorderArgs(argv) {
56183
56011
  async function main() {
56184
56012
  installPipeCloseHandler();
56185
56013
  const program = new Command();
56186
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.232.0");
56014
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.234.0");
56187
56015
  program.addCommand(createBootstrapCommand());
56188
56016
  program.addCommand(createSynthCommand());
56189
56017
  program.addCommand(createListCommand());