@go-to-k/cdkd 0.235.1 → 0.236.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
@@ -2,7 +2,7 @@
2
2
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BzO-joNR.js";
3
3
  import { A as withErrorHandling, C as StackHasActiveImportsError, S as ResourceUpdateNotSupportedError, _ 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-q-WGWgxa.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-B15NAPbL.js";
5
- 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 uploadCfnTemplate, a as withRetry, at as resolveApp, b as IAMRoleProvider, bt as resolveBucketRegion, c as computeImplicitDeleteEdges, ct as resolveSkipPrefix, d as isStatefulRecreateTargetSync, dt as resolveUseCdkBootstrapAssets, et as getDockerImageBySourceHash, f as renderStatefulReason, ft as warnDeprecatedNoPrefixCliFlag, g as gray, gt as findLargeInlineResources, h as cyan, ht as MIGRATE_TMP_PREFIX, i as withResourceDeadline, it as getLegacyStateBucketName, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveStateBucketWithDefault, m as bold, mt as CFN_TEMPLATE_URL_LIMIT, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as synthesisStatusMessage, o as isRetryableTransientError, ot as resolveAutoAssetStorage, p as formatResourceLine, pt as CFN_TEMPLATE_BODY_LIMIT, q as parseBootstrapMarker, r as DeployEngine, rt as getDefaultStateBucketName, s as IMPLICIT_DELETE_DEPENDENCIES, st as resolveCaptureObservedState, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as Synthesizer, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveStateBucketWithDefaultAndSource, v as red, vt as AssemblyReader, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, y as yellow, z as WorkGraph } from "./deploy-engine-iahWC4C1.js";
5
+ import { $ as runDockerStreaming, 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 parseBootstrapMarker, K as ensureAssetStorage, L as AssetPublisher, M as TemplateParser, N as LockManager, O as WAFv2WebACLProvider, P as S3StateBackend, Q as runDockerForeground, R as stringifyValue, S as ProviderRegistry, T as IntrinsicFunctionResolver, U as rewriteTemplateAssetReferences, V as createAssetRedirectResolver, W as AssetModeResolver, X as formatDockerLoginError, Y as buildDockerImage, Z as getDockerCmd, _ as green, _t as findLargeInlineResources, a as withRetry, at as getLegacyStateBucketName, b as IAMRoleProvider, c as computeImplicitDeleteEdges, ct as resolveCaptureObservedState, d as isStatefulRecreateTargetSync, dt as resolveStateBucketWithDefaultAndSource, et as AssetManifestLoader, f as renderStatefulReason, ft as resolveUseCdkBootstrapAssets, g as gray, gt as MIGRATE_TMP_PREFIX, h as cyan, ht as CFN_TEMPLATE_URL_LIMIT, i as withResourceDeadline, it as getDefaultStateBucketName, j as DagBuilder, k as applyRoleArnIfSet, l as extractDeploymentEventError, lt as resolveSkipPrefix, m as bold, mt as CFN_TEMPLATE_BODY_LIMIT, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as Synthesizer, o as isRetryableTransientError, ot as resolveApp, p as formatResourceLine, pt as warnDeprecatedNoPrefixCliFlag, q as getBootstrapMarkerKey, r as DeployEngine, rt as synthesisStatusMessage, s as IMPLICIT_DELETE_DEPENDENCIES, st as resolveAutoAssetStorage, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as getDockerImageBySourceHash, u as MULTI_REGION_RECREATE_BLOCKED_TYPES, ut as resolveStateBucketWithDefault, v as red, vt as uploadCfnTemplate, w as CloudControlProvider, x as collectInlinePolicyNamesManagedBySiblings, xt as resolveBucketRegion, y as yellow, yt as AssemblyReader, z as WorkGraph } from "./deploy-engine-DqAIXGA_.js";
6
6
  import { t as expectedOwnerParam } from "./expected-bucket-owner-BjEfUnkA.js";
7
7
  import { t as ASGProvider } from "./asg-provider-Dbl1_mwT.js";
8
8
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -40,9 +40,9 @@ import graphlib from "graphlib";
40
40
  import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBProxyCommand, CreateDBProxyEndpointCommand, CreateDBSubnetGroupCommand, DBProxyEndpointNotFoundFault, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBProxyCommand, DeleteDBProxyEndpointCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxiesCommand, DescribeDBProxyEndpointsCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBProxyTargetsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyCommand, ModifyDBProxyEndpointCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-rds";
41
41
  import { AddTagsToResourceCommand as AddTagsToResourceCommand$2, CreateCacheClusterCommand, CreateCacheSubnetGroupCommand, DeleteCacheClusterCommand, DeleteCacheSubnetGroupCommand, DescribeCacheClustersCommand, DescribeCacheSubnetGroupsCommand, ElastiCacheClient, ListTagsForResourceCommand as ListTagsForResourceCommand$9, ModifyCacheClusterCommand, ModifyCacheSubnetGroupCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$2 } from "@aws-sdk/client-elasticache";
42
42
  import { Command, Option } from "commander";
43
- import { writeFileSync as writeFileSync$1 } from "fs";
44
43
  import * as readline$1 from "node:readline/promises";
45
44
  import readline from "node:readline/promises";
45
+ import { writeFileSync as writeFileSync$1 } from "fs";
46
46
  import * as zlib from "node:zlib";
47
47
  import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
48
48
  import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetApisCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
@@ -610,6 +610,310 @@ const destroyOptions = [
610
610
  allowUnsupportedTypesOption
611
611
  ];
612
612
 
613
+ //#endregion
614
+ //#region src/cli/commands/bootstrap-destroy.ts
615
+ /**
616
+ * `cdkd bootstrap --destroy` — teardown of cdkd-created account resources
617
+ * (issue #1010, the reverse of `cdkd bootstrap` / `ensureAssetStorage`).
618
+ *
619
+ * Scope per invocation is ONE region's asset storage (`--region`, same
620
+ * option the create side uses): empty + delete the asset bucket, force-
621
+ * delete the container-asset ECR repo, then delete the region's bootstrap
622
+ * marker LAST — the mirror image of the create side's marker-written-last
623
+ * ordering, so a crash mid-teardown leaves the region still consistently
624
+ * opted in (marker present → deploys keep hard-erroring at the missing
625
+ * resources with a re-bootstrap hint, never a silent legacy fallback).
626
+ *
627
+ * The asset bucket / repo NAMES are read from the region's bootstrap
628
+ * marker, NOT recomputed from the naming convention — the marker is the
629
+ * source of truth for names (design §4.1), which keeps this teardown
630
+ * compatible with custom asset-storage names (issue #1011).
631
+ *
632
+ * The state bucket is NOT deleted by default: it is the account's source
633
+ * of truth. `--include-state-bucket` opts in, and even then the deletion
634
+ * is refused while ANY stack state exists in the bucket or any OTHER
635
+ * region is still opted in to asset storage (deleting their markers would
636
+ * silently flip those regions back to legacy mode).
637
+ */
638
+ /** S3 key prefix for state files — same fixed value the create side uses. */
639
+ const STATE_PREFIX = "cdkd";
640
+ /**
641
+ * Every cdkd state file ends with this suffix, regardless of the
642
+ * `--state-prefix` it was deployed under (`{prefix}/{stack}/{region}/state.json`,
643
+ * or legacy `{prefix}/{stack}/state.json`).
644
+ */
645
+ const STATE_FILE_SUFFIX = "/state.json";
646
+ /** `us-east-1` / `ap-northeast-1` / `us-gov-west-1` — a region-shaped segment. */
647
+ const REGION_SEGMENT = /^[a-z]{2}(-[a-z]+)+-\d+$/;
648
+ /**
649
+ * List every state file in the bucket — the WHOLE bucket, not just the
650
+ * default `cdkd/` prefix. Other commands accept `--state-prefix`, so live
651
+ * stack state may exist under ANY prefix in this bucket; scoping this
652
+ * listing to the default prefix would let the reference scan and the
653
+ * `--include-state-bucket` guard silently miss those stacks and delete
654
+ * live state.
655
+ */
656
+ async function listAllStateKeys(stateBackend) {
657
+ return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(STATE_FILE_SUFFIX));
658
+ }
659
+ /**
660
+ * Interactive confirmation for the teardown. Follows the repo's
661
+ * destructive-prompt convention (see `recreate-confirm-prompt.ts`, the
662
+ * same pattern family as `confirm-prompt.ts`): print the deletion plan as
663
+ * a WARN block, `--yes` skips the prompt, a non-TTY stdin without `--yes`
664
+ * is a hard error (never hang / never silently decline in CI), and the
665
+ * prompt itself defaults to NO because the side effect is destructive.
666
+ */
667
+ async function promptBootstrapDestroyConfirm(input) {
668
+ const logger = getLogger();
669
+ logger.warn("");
670
+ logger.warn("cdkd bootstrap --destroy will delete the following:");
671
+ for (const line of input.planLines) logger.warn(` - ${line}`);
672
+ if (input.yes) return true;
673
+ if (process.stdin.isTTY !== true) throw new CdkdError("The bootstrap --destroy confirmation prompt cannot run in a non-interactive environment. Pass --yes / -y to confirm the teardown, or run the command from a real terminal.", "NON_INTERACTIVE_CONFIRM");
674
+ const rl = readline.createInterface({
675
+ input: process.stdin,
676
+ output: process.stdout
677
+ });
678
+ try {
679
+ const trimmed = (await rl.question("\nContinue? (y/N): ")).trim().toLowerCase();
680
+ return trimmed === "y" || trimmed === "yes";
681
+ } finally {
682
+ rl.close();
683
+ }
684
+ }
685
+ /**
686
+ * Scan every state file in the state bucket for references to the target
687
+ * region's asset bucket / container repo. A pragmatic string scan of the
688
+ * serialized state (design rationale in issue #1010): asset references
689
+ * appear in many property shapes (`Code.S3Bucket`, `ImageUri`, nested
690
+ * `TemplateURL`, `s3.Asset` env-var URLs, …) but all of them carry the
691
+ * bucket / repo NAME verbatim, so a substring match over the raw JSON body
692
+ * catches every shape — including ones future providers add — with zero
693
+ * per-shape maintenance. Covers both key layouts (region-prefixed and
694
+ * legacy), nested-stack children, and stacks deployed under a custom
695
+ * `--state-prefix` (the listing spans the whole bucket, not just the
696
+ * default `cdkd/` prefix).
697
+ *
698
+ * Returns a human-readable `stack (region)` descriptor per referencing
699
+ * state file.
700
+ */
701
+ async function scanStateReferences(stateBackend, names) {
702
+ const stateKeys = await listAllStateKeys(stateBackend);
703
+ const referencing = [];
704
+ for (const key of stateKeys) {
705
+ const body = await stateBackend.getRawObject(key);
706
+ if (body === null) continue;
707
+ if (names.some((name) => body.includes(name))) referencing.push(describeStateKey(key));
708
+ }
709
+ return referencing;
710
+ }
711
+ /**
712
+ * `{prefix}/{stack}/{region}/state.json` → `stack (region)`; legacy
713
+ * `{prefix}/{stack}/state.json` → `stack`. The prefix is arbitrary (custom
714
+ * `--state-prefix` values included), so the stack/region pair is derived
715
+ * from the key's TAIL: the last segment is treated as a region only when
716
+ * it is region-shaped.
717
+ */
718
+ function describeStateKey(key) {
719
+ const segments = key.slice(0, -11).split("/");
720
+ const last = segments[segments.length - 1] ?? key;
721
+ const secondLast = segments[segments.length - 2];
722
+ if (secondLast !== void 0 && REGION_SEGMENT.test(last)) return `${secondLast} (${last})`;
723
+ return last;
724
+ }
725
+ /**
726
+ * Empty (all versions + delete markers) and delete an S3 bucket, with
727
+ * `ExpectedBucketOwner` pinned on every call — deleting a foreign bucket
728
+ * that squatted the predictable name would be catastrophic, so the same
729
+ * ownership defense as the create side applies in reverse. Returns `false`
730
+ * (skip, info line) when the bucket does not exist — the idempotency
731
+ * mirror of `ensureAssetStorage`.
732
+ */
733
+ async function emptyAndDeleteBucket(s3Client, bucket, accountId, label, logger) {
734
+ try {
735
+ await s3Client.send(new HeadBucketCommand({
736
+ Bucket: bucket,
737
+ ExpectedBucketOwner: accountId
738
+ }));
739
+ } catch (error) {
740
+ const err = error;
741
+ if (err.name === "NotFound" || err.name === "NoSuchBucket") {
742
+ logger.info(`${label} ${bucket} does not exist — skipping`);
743
+ return false;
744
+ }
745
+ if (err.$metadata?.httpStatusCode === 403) throw new CdkdError(`${label} '${bucket}' exists but is not owned by account ${accountId} (or access is denied). Refusing to delete it.`, "ASSET_STORAGE_FOREIGN_BUCKET", error);
746
+ throw normalizeAwsError(error, {
747
+ bucket,
748
+ operation: "HeadBucket"
749
+ });
750
+ }
751
+ logger.info(`Emptying ${label.toLowerCase()}: ${bucket}`);
752
+ let keyMarker;
753
+ let versionIdMarker;
754
+ do {
755
+ const page = await s3Client.send(new ListObjectVersionsCommand({
756
+ Bucket: bucket,
757
+ ExpectedBucketOwner: accountId,
758
+ ...keyMarker && { KeyMarker: keyMarker },
759
+ ...versionIdMarker && { VersionIdMarker: versionIdMarker }
760
+ }));
761
+ const entries = [...page.Versions ?? [], ...page.DeleteMarkers ?? []].filter((v) => v.Key).map((v) => ({
762
+ Key: v.Key,
763
+ ...v.VersionId && { VersionId: v.VersionId }
764
+ }));
765
+ if (entries.length > 0) {
766
+ const failures = ((await s3Client.send(new DeleteObjectsCommand({
767
+ Bucket: bucket,
768
+ ExpectedBucketOwner: accountId,
769
+ Delete: {
770
+ Objects: entries,
771
+ Quiet: true
772
+ }
773
+ }))).Errors ?? []).map((e) => `${e.Key ?? "<unknown>"} (${e.Code ?? "Error"}: ${e.Message ?? ""})`);
774
+ if (failures.length > 0) throw new CdkdError(`Failed to delete ${failures.length} object(s) from ${label.toLowerCase()} '${bucket}': ${failures.join("; ")}`, "BUCKET_EMPTY_FAILED");
775
+ }
776
+ keyMarker = page.IsTruncated ? page.NextKeyMarker : void 0;
777
+ versionIdMarker = page.IsTruncated ? page.NextVersionIdMarker : void 0;
778
+ } while (keyMarker || versionIdMarker);
779
+ await s3Client.send(new DeleteBucketCommand({
780
+ Bucket: bucket,
781
+ ExpectedBucketOwner: accountId
782
+ }));
783
+ logger.info(`✓ Deleted ${label.toLowerCase()}: ${bucket}`);
784
+ return true;
785
+ }
786
+ /**
787
+ * Force-delete the container-asset ECR repository. Missing repo is an
788
+ * idempotent skip with an info line.
789
+ */
790
+ async function deleteContainerRepo(containerRepo, region, profile, logger) {
791
+ const ecrClient = new ECRClient({
792
+ region,
793
+ ...profile && { profile }
794
+ });
795
+ try {
796
+ await ecrClient.send(new DeleteRepositoryCommand({
797
+ repositoryName: containerRepo,
798
+ force: true
799
+ }));
800
+ logger.info(`✓ Deleted container-asset ECR repository: ${containerRepo}`);
801
+ } catch (error) {
802
+ if (error.name === "RepositoryNotFoundException") logger.info(`Container-asset repository ${containerRepo} does not exist — skipping`);
803
+ else throw normalizeAwsError(error, { operation: "DeleteRepository" });
804
+ } finally {
805
+ ecrClient.destroy();
806
+ }
807
+ }
808
+ /**
809
+ * List OTHER regions that still have a bootstrap marker in the state
810
+ * bucket (i.e. are still opted in to cdkd asset storage).
811
+ */
812
+ async function listOtherBootstrapRegions(stateBackend, region) {
813
+ return (await stateBackend.listRawKeys(BOOTSTRAP_MARKER_PREFIX)).filter((k) => k.endsWith(".json")).map((k) => k.slice(BOOTSTRAP_MARKER_PREFIX.length, -5)).filter((r) => r.length > 0 && r !== region);
814
+ }
815
+ /**
816
+ * `cdkd bootstrap --destroy` implementation. Dispatched from the bootstrap
817
+ * command action when `--destroy` is passed.
818
+ */
819
+ async function bootstrapDestroyCommand(options) {
820
+ const logger = getLogger();
821
+ if (options.verbose) logger.setLevel("debug");
822
+ logger.info("Starting cdkd bootstrap --destroy...");
823
+ logger.debug("Options:", options);
824
+ await applyRoleArnIfSet({
825
+ roleArn: options.roleArn,
826
+ region: options.region
827
+ });
828
+ const awsClients = new AwsClients({
829
+ ...options.region && { region: options.region },
830
+ ...options.profile && { profile: options.profile }
831
+ });
832
+ setAwsClients(awsClients);
833
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
834
+ const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
835
+ const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
836
+ const markerS3Client = new S3Client({
837
+ region,
838
+ ...options.profile && { profile: options.profile }
839
+ });
840
+ const stateBackend = new S3StateBackend(markerS3Client, {
841
+ bucket: bucketName,
842
+ prefix: STATE_PREFIX
843
+ }, {
844
+ region,
845
+ ...options.profile && { profile: options.profile }
846
+ });
847
+ try {
848
+ const markerKey = getBootstrapMarkerKey(region);
849
+ let markerBody;
850
+ try {
851
+ markerBody = await stateBackend.getRawObject(markerKey);
852
+ } catch (error) {
853
+ if (error.name === "NoSuchBucket") {
854
+ logger.info(`State bucket '${bucketName}' does not exist — this account/region was never bootstrapped; nothing to delete.`);
855
+ return;
856
+ }
857
+ throw error;
858
+ }
859
+ let marker;
860
+ if (markerBody === null) {
861
+ logger.info(`No bootstrap marker for region '${region}' (${markerKey}) — asset storage is not opted in for this region (or was already destroyed); nothing to delete.`);
862
+ if (!options.includeStateBucket) {
863
+ logger.info(`If the asset bucket / ECR repo still exist without a marker, re-run 'cdkd bootstrap --region ${region}' to recreate the marker, then destroy again.`);
864
+ return;
865
+ }
866
+ } else marker = parseBootstrapMarker(markerBody, markerKey);
867
+ if (marker && !options.force) {
868
+ logger.info("Scanning stack state for references to the asset storage...");
869
+ const referencing = await scanStateReferences(stateBackend, [marker.assetBucket, marker.containerRepo]);
870
+ if (referencing.length > 0) throw new CdkdError(`Refusing to destroy asset storage for region '${region}': ${referencing.length} deployed stack(s) still reference '${marker.assetBucket}' / '${marker.containerRepo}':\n` + referencing.map((s) => ` - ${s}`).join("\n") + "\nDestroy (or re-deploy off the storage) these stacks first, or pass --force to delete anyway.", "ASSET_STORAGE_IN_USE");
871
+ }
872
+ if (options.includeStateBucket) {
873
+ const stateKeys = await listAllStateKeys(stateBackend);
874
+ if (stateKeys.length > 0) {
875
+ const listing = stateKeys.map((k) => ` - ${describeStateKey(k)} [${k}]`).join("\n");
876
+ throw new CdkdError(`Refusing to delete state bucket '${bucketName}': ${stateKeys.length} stack(s) still have state in it:\n${listing}\nDestroy every stack ('cdkd destroy' / 'cdkd state destroy') before deleting the state bucket.`, "STATE_BUCKET_NOT_EMPTY");
877
+ }
878
+ const otherRegions = await listOtherBootstrapRegions(stateBackend, region);
879
+ if (otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${otherRegions.join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
880
+ }
881
+ const planLines = [];
882
+ if (marker) {
883
+ planLines.push(`Asset bucket: s3://${marker.assetBucket} (region ${region}, all contents)`);
884
+ planLines.push(`Container-asset ECR repository: ${marker.containerRepo} (region ${region}, all images)`);
885
+ planLines.push(`Bootstrap marker: s3://${bucketName}/${markerKey} (deleted last)`);
886
+ }
887
+ if (options.includeStateBucket) planLines.push(`State bucket: s3://${bucketName} (ALL contents, including all versions)`);
888
+ if (!await promptBootstrapDestroyConfirm({
889
+ planLines,
890
+ yes: options.yes
891
+ })) {
892
+ logger.info("Bootstrap teardown cancelled — nothing deleted.");
893
+ return;
894
+ }
895
+ if (marker) {
896
+ await emptyAndDeleteBucket(awsClients.s3, marker.assetBucket, accountId, "Asset bucket", logger);
897
+ await deleteContainerRepo(marker.containerRepo, region, options.profile, logger);
898
+ await stateBackend.deleteRawObjects([markerKey]);
899
+ logger.info(`✓ Deleted bootstrap marker (${markerKey})`);
900
+ logger.info(`\ncdkd asset storage is now OFF for region ${region}: future deploys in this region fall back to legacy mode (CDK bootstrap destinations) unless the region is bootstrapped again.`);
901
+ }
902
+ if (options.includeStateBucket) {
903
+ const rebuiltStateBucketClient = await rebuildClientForBucketRegion(awsClients.s3, bucketName, { ...options.profile && { profile: options.profile } });
904
+ try {
905
+ await emptyAndDeleteBucket(rebuiltStateBucketClient ?? awsClients.s3, bucketName, accountId, "State bucket", logger);
906
+ } finally {
907
+ rebuiltStateBucketClient?.destroy();
908
+ }
909
+ }
910
+ logger.info("\n✓ Bootstrap teardown completed");
911
+ } finally {
912
+ markerS3Client.destroy();
913
+ awsClients.destroy();
914
+ }
915
+ }
916
+
613
917
  //#endregion
614
918
  //#region src/cli/commands/bootstrap.ts
615
919
  /**
@@ -772,7 +1076,24 @@ async function bootstrapCommand(options) {
772
1076
  * Create bootstrap command
773
1077
  */
774
1078
  function createBootstrapCommand() {
775
- const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating the S3 state bucket plus cdkd-owned asset storage (asset bucket + container-asset ECR repo) for the region").option("--state-bucket <bucket>", "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})").option("--force", "Force reconfiguration of existing bucket", false).option("--no-assets", "Skip cdkd asset storage (asset bucket / ECR repo / marker) — deploys keep publishing assets to the CDK bootstrap bucket/repo").addOption(new Option("--region <region>", "AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)")).action(withErrorHandling(bootstrapCommand));
1079
+ const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating the S3 state bucket plus cdkd-owned asset storage (asset bucket + container-asset ECR repo) for the region. With --destroy, tear down the region's asset storage instead (issue #1010).").option("--state-bucket <bucket>", "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})").option("--force", "Create: force reconfiguration of an existing bucket. Destroy: delete the asset storage even when deployed stacks still reference it", false).option("--no-assets", "Skip cdkd asset storage (asset bucket / ECR repo / marker) — deploys keep publishing assets to the CDK bootstrap bucket/repo").option("--destroy", "Tear down the region's cdkd asset storage (empty + delete the asset bucket, force-delete the container-asset ECR repo, then delete the bootstrap marker last). The state bucket is kept unless --include-state-bucket is also passed", false).option("--include-state-bucket", "With --destroy: also delete the S3 state bucket. Refused while any stack state exists or any other region is still opted in to asset storage", false).addOption(new Option("--region <region>", "AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)")).action(withErrorHandling(async (options) => {
1080
+ if (options.destroy) {
1081
+ if (!options.assets) throw new CdkdError("--no-assets cannot be combined with --destroy. The teardown scope is the asset storage itself; use --include-state-bucket to also delete the state bucket.", "INVALID_OPTIONS");
1082
+ await bootstrapDestroyCommand({
1083
+ ...options.stateBucket && { stateBucket: options.stateBucket },
1084
+ ...options.region && { region: options.region },
1085
+ ...options.profile && { profile: options.profile },
1086
+ ...options.roleArn && { roleArn: options.roleArn },
1087
+ force: options.force,
1088
+ includeStateBucket: options.includeStateBucket,
1089
+ yes: options.yes,
1090
+ verbose: options.verbose
1091
+ });
1092
+ return;
1093
+ }
1094
+ if (options.includeStateBucket) throw new CdkdError("--include-state-bucket requires --destroy (it opts the state bucket into the teardown).", "INVALID_OPTIONS");
1095
+ await bootstrapCommand(options);
1096
+ }));
776
1097
  commonOptions.forEach((opt) => cmd.addOption(opt));
777
1098
  return cmd;
778
1099
  }
@@ -1534,7 +1855,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1534
1855
  const FLUSH_EVENT_THRESHOLD = 50;
1535
1856
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1536
1857
  function getCdkdVersion() {
1537
- return "0.235.1";
1858
+ return "0.236.0";
1538
1859
  }
1539
1860
  /**
1540
1861
  * Generate a time-sortable unique run id, e.g.
@@ -56083,7 +56404,7 @@ function reorderArgs(argv) {
56083
56404
  async function main() {
56084
56405
  installPipeCloseHandler();
56085
56406
  const program = new Command();
56086
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.235.1");
56407
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.236.0");
56087
56408
  program.addCommand(createBootstrapCommand());
56088
56409
  program.addCommand(createSynthCommand());
56089
56410
  program.addCommand(createListCommand());