@go-to-k/cdkd 0.143.0 → 0.144.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,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-EtWSTAje.js";
3
- import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-Dff3_JMn.js";
3
+ import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as MIGRATE_TMP_PREFIX, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, J as AssemblyReader, K as findLargeInlineResources, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as CdkdError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as CFN_TEMPLATE_BODY_LIMIT, V as resolveStateBucketWithDefaultAndSource, W as CFN_TEMPLATE_URL_LIMIT, X as resolveBucketRegion, _ as normalizeAwsTagsToCfn, _t as normalizeAwsError, a as withRetry, at as MissingCdkCliError, b as CloudControlProvider, c as cyan, ct as ResourceTimeoutError, d as red, dt as StackHasActiveImportsError, f as yellow, ft as StackTerminationProtectionError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, j as stringifyValue, k as shouldRetainResource, l as gray, lt as ResourceUpdateNotSupportedError, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LocalMigrateError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as PartialFailureError, p as IAMRoleProvider, q as uploadCfnTemplate, r as DeployEngine, rt as LocalStartServiceError, s as bold, st as ProvisioningError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LocalInvokeBuildError, u as green, ut as RouteDiscoveryError, v as resolveExplicitPhysicalId, vt as withErrorHandling, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-DjnWyAAc.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-BF03Alpe.js";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, 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";
7
8
  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";
@@ -20,6 +21,7 @@ import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDi
20
21
  import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6 } from "@aws-sdk/client-cloudwatch";
21
22
  import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch-logs";
22
23
  import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
24
+ import * as fs from "node:fs";
23
25
  import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
24
26
  import * as path from "node:path";
25
27
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
@@ -30944,6 +30946,602 @@ function mapNotificationsToCfn(configurations) {
30944
30946
  return result;
30945
30947
  }
30946
30948
 
30949
+ //#endregion
30950
+ //#region src/cli/commands/destroy-runner.ts
30951
+ /**
30952
+ * Resource-type → state-property name pairs that gate AWS deletion
30953
+ * protection. Used by the `--remove-protection` confirmation prompt to
30954
+ * report a best-effort count of resources that will have protection
30955
+ * cleared. The actual flip-off is unconditional inside each provider's
30956
+ * `delete()` (idempotent — safe when AWS already has protection off),
30957
+ * so the count is informational only.
30958
+ *
30959
+ * Most types use a boolean flag — the value `true` is what we count.
30960
+ * Two types use a string-valued enum (Cognito UserPool's
30961
+ * `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
30962
+ * `DeletionProtection` is `'none' | 'prevent-force-deletion' |
30963
+ * 'prevent-all-deletion'`). For those, the helper checks against a
30964
+ * per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
30965
+ *
30966
+ * Exported for unit-test coverage of `countProtectedResources`.
30967
+ */
30968
+ const PROTECTION_PROPERTY_BY_TYPE = {
30969
+ "AWS::Logs::LogGroup": "DeletionProtectionEnabled",
30970
+ "AWS::RDS::DBInstance": "DeletionProtection",
30971
+ "AWS::RDS::DBCluster": "DeletionProtection",
30972
+ "AWS::DocDB::DBCluster": "DeletionProtection",
30973
+ "AWS::Neptune::DBCluster": "DeletionProtection",
30974
+ "AWS::Neptune::DBInstance": "DeletionProtection",
30975
+ "AWS::DynamoDB::Table": "DeletionProtectionEnabled",
30976
+ "AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
30977
+ "AWS::EC2::Instance": "DisableApiTermination",
30978
+ "AWS::Cognito::UserPool": "DeletionProtection",
30979
+ "AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
30980
+ };
30981
+ /**
30982
+ * For string-valued protection enums, the set of values that count as
30983
+ * "currently protected". Types absent from this map use the default
30984
+ * (boolean `true`).
30985
+ */
30986
+ const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
30987
+ "AWS::Cognito::UserPool": new Set(["ACTIVE"]),
30988
+ "AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
30989
+ };
30990
+ /**
30991
+ * Count how many resources in a stack's recorded state appear to have
30992
+ * deletion protection enabled. Walks `properties` and `observedProperties`
30993
+ * for the property name registered against each resource type in
30994
+ * `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
30995
+ * `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
30996
+ * handled separately via the `deletion_protection.enabled` key.
30997
+ */
30998
+ function countProtectedResources(state) {
30999
+ let count = 0;
31000
+ for (const resource of Object.values(state.resources ?? {})) {
31001
+ const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
31002
+ if (propName) {
31003
+ const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
31004
+ const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
31005
+ if (activeValues) {
31006
+ if (activeValues.has(recorded)) count++;
31007
+ } else if (recorded === true) count++;
31008
+ continue;
31009
+ }
31010
+ if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
31011
+ if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
31012
+ }
31013
+ }
31014
+ return count;
31015
+ }
31016
+ /**
31017
+ * Run the destroy lifecycle for one stack against an already-loaded
31018
+ * `StackState`, reusing the caller's state backend / lock manager.
31019
+ *
31020
+ * Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
31021
+ * can call into the exact same per-stack pipeline without depending on
31022
+ * synth or the CDK app. The state-source split is the only meaningful
31023
+ * difference between the two commands — everything from "prompt the user"
31024
+ * onwards is identical.
31025
+ *
31026
+ * Side effects:
31027
+ * - Acquires (and releases) the stack's S3 lock.
31028
+ * - Switches `process.env.AWS_REGION` for the duration of the destroy when
31029
+ * the stack's recorded region differs from `baseRegion`. Restored in the
31030
+ * `finally` block.
31031
+ * - On full success, deletes the state file. On any failure, the state
31032
+ * file is preserved so the user can retry.
31033
+ */
31034
+ async function runDestroyForStack(stackName, state, ctx) {
31035
+ const logger = getLogger();
31036
+ const result = {
31037
+ stackName,
31038
+ cancelled: false,
31039
+ skippedEmpty: false,
31040
+ deletedCount: 0,
31041
+ retainedCount: 0,
31042
+ errorCount: 0
31043
+ };
31044
+ const resourceCount = Object.keys(state.resources).length;
31045
+ const regionForState = state.region ?? ctx.baseRegion;
31046
+ if (resourceCount === 0) {
31047
+ logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
31048
+ await ctx.stateBackend.deleteState(stackName, regionForState);
31049
+ logger.info(`${green("✓")} State deleted`);
31050
+ result.skippedEmpty = true;
31051
+ return result;
31052
+ }
31053
+ const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
31054
+ if (needsStrongRefCheck) {
31055
+ const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
31056
+ if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
31057
+ }
31058
+ logger.info(`\nResources to be deleted (${resourceCount}):`);
31059
+ for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
31060
+ const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
31061
+ if (!ctx.skipConfirmation) {
31062
+ const rl = readline.createInterface({
31063
+ input: process.stdin,
31064
+ output: process.stdout
31065
+ });
31066
+ const prompt = ctx.removeProtection ? `\nAbout to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `\nAre you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
31067
+ const answer = await rl.question(prompt);
31068
+ rl.close();
31069
+ const trimmed = answer.trim().toLowerCase();
31070
+ if (ctx.removeProtection) {
31071
+ if (trimmed !== "y" && trimmed !== "yes") {
31072
+ logger.info("Destroy cancelled");
31073
+ result.cancelled = true;
31074
+ return result;
31075
+ }
31076
+ } else if (trimmed === "n" || trimmed === "no") {
31077
+ logger.info("Destroy cancelled");
31078
+ result.cancelled = true;
31079
+ return result;
31080
+ }
31081
+ }
31082
+ const stackRegion = state.region;
31083
+ let destroyProviderRegistry = ctx.providerRegistry;
31084
+ let destroyAwsClients;
31085
+ if (stackRegion && stackRegion !== ctx.baseRegion) {
31086
+ logger.info(`Stack region: ${stackRegion}`);
31087
+ process.env["AWS_REGION"] = stackRegion;
31088
+ process.env["AWS_DEFAULT_REGION"] = stackRegion;
31089
+ destroyAwsClients = new AwsClients({
31090
+ region: stackRegion,
31091
+ ...ctx.profile && { profile: ctx.profile }
31092
+ });
31093
+ setAwsClients(destroyAwsClients);
31094
+ destroyProviderRegistry = new ProviderRegistry();
31095
+ registerAllProviders(destroyProviderRegistry);
31096
+ destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
31097
+ }
31098
+ logger.info(`\nAcquiring lock for stack ${stackName}...`);
31099
+ await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
31100
+ if (needsStrongRefCheck) {
31101
+ const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
31102
+ if (consumers.length > 0) {
31103
+ try {
31104
+ await ctx.lockManager.releaseLock(stackName, regionForState);
31105
+ } catch (releaseErr) {
31106
+ logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
31107
+ }
31108
+ throw new StackHasActiveImportsError(stackName, regionForState, consumers);
31109
+ }
31110
+ }
31111
+ const renderer = getLiveRenderer();
31112
+ renderer.start();
31113
+ try {
31114
+ logger.info("Building dependency graph...");
31115
+ const template = {
31116
+ AWSTemplateFormatVersion: "2010-09-09",
31117
+ Resources: {}
31118
+ };
31119
+ for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
31120
+ Type: resource.resourceType,
31121
+ Properties: resource.properties || {},
31122
+ ...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
31123
+ };
31124
+ const typeToLogicalIds = /* @__PURE__ */ new Map();
31125
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
31126
+ const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
31127
+ ids.push(logicalId);
31128
+ typeToLogicalIds.set(resource.resourceType, ids);
31129
+ }
31130
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
31131
+ const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
31132
+ if (!mustDeleteAfter) continue;
31133
+ for (const depType of mustDeleteAfter) {
31134
+ const depIds = typeToLogicalIds.get(depType);
31135
+ if (!depIds) continue;
31136
+ for (const depId of depIds) {
31137
+ const existing = template.Resources[depId]?.DependsOn ?? [];
31138
+ const depsArray = Array.isArray(existing) ? existing : [existing];
31139
+ if (!depsArray.includes(logicalId)) {
31140
+ template.Resources[depId] = {
31141
+ ...template.Resources[depId],
31142
+ DependsOn: [...depsArray, logicalId]
31143
+ };
31144
+ logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
31145
+ }
31146
+ }
31147
+ }
31148
+ }
31149
+ const dagBuilder = new DagBuilder();
31150
+ const graph = dagBuilder.buildGraph(template);
31151
+ const executionLevels = dagBuilder.getExecutionLevels(graph);
31152
+ logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
31153
+ for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
31154
+ const level = executionLevels[levelIndex];
31155
+ if (!level) continue;
31156
+ logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
31157
+ const stackRegion = state.region ?? ctx.baseRegion;
31158
+ const deletePromises = level.map(async (logicalId) => {
31159
+ const resource = state.resources[logicalId];
31160
+ if (!resource) {
31161
+ logger.warn(`Resource ${logicalId} not found in state, skipping`);
31162
+ return;
31163
+ }
31164
+ if (shouldRetainResource(resource.deletionPolicy)) {
31165
+ logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
31166
+ result.retainedCount++;
31167
+ return;
31168
+ }
31169
+ const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
31170
+ renderer.addTask(logicalId, baseLabel);
31171
+ try {
31172
+ const provider = destroyProviderRegistry.getProvider(resource.resourceType);
31173
+ const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
31174
+ const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
31175
+ const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
31176
+ const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
31177
+ await withResourceDeadline(async () => {
31178
+ const maxAttempts = provider.disableOuterRetry ? 0 : 3;
31179
+ let lastDeleteError;
31180
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
31181
+ await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
31182
+ ...state.region !== void 0 && { expectedRegion: state.region },
31183
+ ...ctx.removeProtection === true && { removeProtection: true }
31184
+ });
31185
+ lastDeleteError = null;
31186
+ break;
31187
+ } catch (retryError) {
31188
+ lastDeleteError = retryError;
31189
+ const msg = retryError instanceof Error ? retryError.message : String(retryError);
31190
+ if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
31191
+ const delay = 5e3 * Math.pow(2, attempt);
31192
+ logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
31193
+ await new Promise((resolve) => setTimeout(resolve, delay));
31194
+ }
31195
+ if (lastDeleteError) throw lastDeleteError;
31196
+ }, {
31197
+ warnAfterMs,
31198
+ timeoutMs,
31199
+ onWarn: (elapsedMs) => {
31200
+ const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
31201
+ renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
31202
+ renderer.printAbove(() => {
31203
+ logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
31204
+ });
31205
+ },
31206
+ onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
31207
+ });
31208
+ renderer.removeTask(logicalId);
31209
+ logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
31210
+ result.deletedCount++;
31211
+ } catch (error) {
31212
+ renderer.removeTask(logicalId);
31213
+ const msg = error instanceof Error ? error.message : String(error);
31214
+ if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
31215
+ logger.debug(` ${logicalId} already deleted, removing from state`);
31216
+ result.deletedCount++;
31217
+ } else if (error instanceof ResourceTimeoutError) {
31218
+ const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
31219
+ logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
31220
+ result.errorCount++;
31221
+ } else {
31222
+ logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
31223
+ result.errorCount++;
31224
+ }
31225
+ } finally {
31226
+ renderer.removeTask(logicalId);
31227
+ }
31228
+ });
31229
+ await Promise.all(deletePromises);
31230
+ }
31231
+ if (result.errorCount === 0) {
31232
+ await ctx.stateBackend.deleteState(stackName, regionForState);
31233
+ logger.debug("State deleted");
31234
+ if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
31235
+ } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
31236
+ const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
31237
+ if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
31238
+ 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.`);
31239
+ } finally {
31240
+ renderer.stop();
31241
+ logger.debug("Releasing lock...");
31242
+ await ctx.lockManager.releaseLock(stackName, regionForState);
31243
+ if (destroyAwsClients) {
31244
+ destroyAwsClients.destroy();
31245
+ process.env["AWS_REGION"] = ctx.baseRegion;
31246
+ process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
31247
+ setAwsClients(ctx.baseAwsClients);
31248
+ }
31249
+ }
31250
+ return result;
31251
+ }
31252
+ /**
31253
+ * Strong-reference scan: read every other stack's state.json from the
31254
+ * state bucket and check whether any of its `imports[]` entries names
31255
+ * `producerStack`. Returns the list of offending consumers (possibly
31256
+ * empty).
31257
+ *
31258
+ * NEVER trusts the persistent exports index — a stale index could miss
31259
+ * a freshly-recorded consumer and let a destructive destroy through.
31260
+ * The cost is one `listStacks` + N parallel GETs at destroy time only
31261
+ * (not the deploy hot path), which the user-facing UX rationalizes as
31262
+ * the "destroy is slow OK" trade-off (Issue #343).
31263
+ */
31264
+ async function scanActiveConsumers(producerStack, producerRegion, ctx) {
31265
+ const refs = await ctx.stateBackend.listStacks();
31266
+ return (await Promise.all(refs.map(async (ref) => {
31267
+ const region = ref.region ?? ctx.baseRegion;
31268
+ if (ref.stackName === producerStack && region === producerRegion) return null;
31269
+ try {
31270
+ const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
31271
+ if (!imports || imports.length === 0) return null;
31272
+ const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
31273
+ if (matches.length === 0) return null;
31274
+ return matches.map((entry) => ({
31275
+ consumerStack: ref.stackName,
31276
+ consumerRegion: region,
31277
+ exportName: entry.exportName
31278
+ }));
31279
+ } catch {
31280
+ return null;
31281
+ }
31282
+ }))).filter((r) => r !== null).flat();
31283
+ }
31284
+
31285
+ //#endregion
31286
+ //#region src/provisioning/nested-stack-context.ts
31287
+ const storage = new AsyncLocalStorage();
31288
+ /**
31289
+ * Run `fn` inside a NestedStackProvider context scope. Calls to
31290
+ * `getCurrentNestedStackContext()` from inside `fn` (and any awaited callees)
31291
+ * return `ctx`. Nested scopes shadow outer ones — the recursive provider
31292
+ * uses this to switch the "current parent" to the child before kicking off
31293
+ * the child's deploy / destroy, so grand-nested handling resolves against
31294
+ * the right parent.
31295
+ */
31296
+ function withNestedStackContext(ctx, fn) {
31297
+ return storage.run(ctx, fn);
31298
+ }
31299
+ /**
31300
+ * Returns the current `NestedStackProviderContext`, or `undefined` when called
31301
+ * outside any `withNestedStackContext` scope (= cdkd is operating on a
31302
+ * top-level stack with no nested-stack work in flight).
31303
+ *
31304
+ * `NestedStackProvider.create / update / delete` MUST find a context here —
31305
+ * absence means a caller forgot to wrap the deploy / destroy entry point.
31306
+ */
31307
+ function getCurrentNestedStackContext() {
31308
+ return storage.getStore();
31309
+ }
31310
+
31311
+ //#endregion
31312
+ //#region src/provisioning/providers/nested-stack-provider.ts
31313
+ /**
31314
+ * Provider for `AWS::CloudFormation::Stack` — cdkd's recursive nested-stack
31315
+ * adapter. Issue [#459](https://github.com/go-to-k/cdkd/issues/459); see
31316
+ * [docs/design/459-nested-stacks.md](../../../docs/design/459-nested-stacks.md)
31317
+ * for the full design.
31318
+ *
31319
+ * On `create` / `update`, the provider builds a child {@link DeployEngine}
31320
+ * against the same shared state backend / lock manager / provider registry,
31321
+ * deploys the child template recursively, and surfaces the child's outputs
31322
+ * as `attributes['Outputs.<Key>']` so the parent's
31323
+ * `Fn::GetAtt: [<NestedStack>, 'Outputs.<Key>']` references resolve via
31324
+ * the existing flat-key fast path in {@link IntrinsicFunctionResolver}.
31325
+ *
31326
+ * On `delete`, the provider loads the child's state and routes it through
31327
+ * {@link runDestroyForStack} for a regular reverse-DAG destroy — the same
31328
+ * code `cdkd destroy` uses on a top-level stack.
31329
+ *
31330
+ * The child's state file lives at
31331
+ * `cdkd/<parentStackName>~<NestedStackLogicalId>/<region>/state.json`
31332
+ * (the `~` separator is rare in CDK logical ids; verified safe against
31333
+ * CDK Stage paths which use `/`). The synthesized `physicalId` is a fake
31334
+ * ARN with `cdkd-local` partition so any downstream consumer that
31335
+ * accidentally uses it as a real AWS ARN fails loudly.
31336
+ */
31337
+ var NestedStackProvider = class {
31338
+ logger = getLogger().child("NestedStackProvider");
31339
+ /**
31340
+ * Opt out of the deploy engine's outer transient-error retry loop. A
31341
+ * nested-stack `create` recursively spawns a child {@link DeployEngine}
31342
+ * that has its own retry / rollback machinery; an outer retry would
31343
+ * re-enter the entire child deploy on a transient error and produce
31344
+ * duplicate AWS resources before the second attempt's per-resource
31345
+ * state save settles. The child engine handles transient errors
31346
+ * internally — mirroring the same opt-out the Custom Resource provider
31347
+ * uses for the same reason.
31348
+ */
31349
+ disableOuterRetry = true;
31350
+ /**
31351
+ * The CC API fallback path for `AWS::CloudFormation::Stack` would call
31352
+ * CloudFormation's own `CreateStack` — defeating cdkd's whole "no CFn"
31353
+ * approach for the nested children. Refuse the fallback so any future
31354
+ * regression that drops a real property from `handledProperties`
31355
+ * surfaces as an explicit "unhandled property" deploy error instead of
31356
+ * silently round-tripping through CloudFormation.
31357
+ */
31358
+ disableCcApiFallback = true;
31359
+ /**
31360
+ * Properties this provider actually wires through to the child deploy.
31361
+ * `TemplateURL` is the asset-published S3 URL of the child template
31362
+ * (cdkd reads the local template file via `Metadata['aws:asset:path']`,
31363
+ * so the URL itself is informational here); `Parameters` is the typed
31364
+ * parameter map forwarded as `DeployEngineOptions.parameters` to the
31365
+ * child engine.
31366
+ */
31367
+ handledProperties = new Map([["AWS::CloudFormation::Stack", new Set(["TemplateURL", "Parameters"])]]);
31368
+ /**
31369
+ * Every other property on `AWS::CloudFormation::Stack` is intentionally
31370
+ * not threaded through — cdkd does not go through CloudFormation, so
31371
+ * CFn-only inputs (rollback / capability / role / notification /
31372
+ * termination-protection / stack-update policy / per-stack timeout /
31373
+ * tags) have no equivalent. The synthesized `Ref` ARN is a placeholder,
31374
+ * not a real AWS resource — so `Tags` and `Description` similarly
31375
+ * have nothing to attach to. `StackName` is replaced by cdkd's derived
31376
+ * `<parent>~<logicalId>` key per design §3, and `TemplateBody` is
31377
+ * superseded by the local `Metadata['aws:asset:path']` lookup.
31378
+ */
31379
+ unhandledByDesign = new Map([["AWS::CloudFormation::Stack", new Map([
31380
+ ["TemplateBody", "CFn-only inline template — cdkd reads the child template from the synth output via Metadata['aws:asset:path'] instead of accepting it inline"],
31381
+ ["Capabilities", "CFn-only IAM capability declaration — cdkd does not go through CloudFormation so capabilities have no equivalent"],
31382
+ ["Description", "CFn-only informational — no semantic effect on the recursive deploy"],
31383
+ ["DisableRollback", "CFn-only — cdkd controls rollback via the top-level deploy-engine --no-rollback flag, not per nested stack"],
31384
+ ["EnableTerminationProtection", "CFn-only per-nested-stack flag — cdkd records stack-level terminationProtection at CDK synth time (parent only) and `cdkd destroy` consults that for refusal"],
31385
+ ["NotificationARNs", "CFn-only SNS-on-stack-event surface — cdkd has no equivalent (issue #459 design §9)"],
31386
+ ["RoleARN", "CFn-only role-assumption — cdkd uses the caller credentials directly, no per-resource role assumption"],
31387
+ ["StackName", "cdkd derives the child stack name as `<parent>~<logicalId>` per design §3 (state-key uniqueness); a user-provided StackName has no effect"],
31388
+ ["StackPolicyBody", "CFn-only stack-update policy — cdkd has no equivalent (per-resource diff replaces stack-level policy)"],
31389
+ ["StackPolicyURL", "CFn-only stack-update policy URL — cdkd has no equivalent"],
31390
+ ["StackStatusReason", "CFn-only read-only output — never a real input property"],
31391
+ ["Tags", "CFn-only — cdkd does not tag the synthesized \"stack\" (the parent's synthesized ARN is a cdkd-local placeholder, not a real AWS resource)"],
31392
+ ["TimeoutInMinutes", "CFn-only stack-create deadline — cdkd uses per-resource --resource-timeout instead (issue #459 design §9)"]
31393
+ ])]]);
31394
+ async create(logicalId, _resourceType, properties) {
31395
+ const ctx = this.requireContext();
31396
+ this.requireDeployContext(ctx, "create");
31397
+ const childTemplatePath = ctx.nestedTemplates[logicalId];
31398
+ 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).`);
31399
+ const childTemplate = this.readChildTemplate(childTemplatePath);
31400
+ const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
31401
+ const childRegion = ctx.parentRegion;
31402
+ const childParameters = this.extractParameters(properties);
31403
+ const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
31404
+ const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
31405
+ this.logger.info(`Deploying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
31406
+ await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
31407
+ const attributes = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
31408
+ return {
31409
+ physicalId: this.synthesizeArn(ctx.accountId, ctx.parentRegion, ctx.parentStackName, logicalId),
31410
+ attributes
31411
+ };
31412
+ }
31413
+ async update(logicalId, physicalId, _resourceType, properties, _previousProperties) {
31414
+ const ctx = this.requireContext();
31415
+ this.requireDeployContext(ctx, "update");
31416
+ const childTemplatePath = ctx.nestedTemplates[logicalId];
31417
+ if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' on update.`);
31418
+ const childTemplate = this.readChildTemplate(childTemplatePath);
31419
+ const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
31420
+ const childRegion = ctx.parentRegion;
31421
+ const childParameters = this.extractParameters(properties);
31422
+ const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
31423
+ const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
31424
+ this.logger.info(`Updating nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
31425
+ await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
31426
+ return {
31427
+ physicalId,
31428
+ wasReplaced: false,
31429
+ attributes: await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion)
31430
+ };
31431
+ }
31432
+ async delete(logicalId, _physicalId, _resourceType, _properties, deleteContext) {
31433
+ const ctx = this.requireContext();
31434
+ const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
31435
+ const childRegion = ctx.parentRegion;
31436
+ const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
31437
+ if (!childStateData) {
31438
+ this.logger.debug(`Nested stack ${childStackName} has no state — treating delete as idempotent success.`);
31439
+ return;
31440
+ }
31441
+ const resourceCount = Object.keys(childStateData.state.resources).length;
31442
+ this.logger.info(`Destroying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
31443
+ await withNestedStackContext({
31444
+ ...ctx,
31445
+ parentStackName: childStackName,
31446
+ parentRegion: childRegion,
31447
+ nestedTemplates: void 0
31448
+ }, () => runDestroyForStack(childStackName, childStateData.state, {
31449
+ stateBackend: ctx.stateBackend,
31450
+ lockManager: ctx.lockManager,
31451
+ providerRegistry: ctx.providerRegistry,
31452
+ baseAwsClients: ctx.awsClients,
31453
+ baseRegion: childRegion,
31454
+ stateBucket: ctx.stateBucket,
31455
+ skipConfirmation: true,
31456
+ ...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
31457
+ ...ctx.destroyOptions?.profile && { profile: ctx.destroyOptions.profile },
31458
+ ...deleteContext?.removeProtection === true && { removeProtection: true },
31459
+ ...ctx.destroyOptions?.resourceWarnAfterMs !== void 0 && { resourceWarnAfterMs: ctx.destroyOptions.resourceWarnAfterMs },
31460
+ ...ctx.destroyOptions?.resourceTimeoutMs !== void 0 && { resourceTimeoutMs: ctx.destroyOptions.resourceTimeoutMs },
31461
+ ...ctx.destroyOptions?.resourceWarnAfterByType && { resourceWarnAfterByType: ctx.destroyOptions.resourceWarnAfterByType },
31462
+ ...ctx.destroyOptions?.resourceTimeoutByType && { resourceTimeoutByType: ctx.destroyOptions.resourceTimeoutByType }
31463
+ }));
31464
+ }
31465
+ async getAttribute(_physicalId, _resourceType, attributeName) {
31466
+ throw new Error(`AWS::CloudFormation::Stack: attribute '${attributeName}' is not in the recorded Outputs map. Only 'Outputs.<Key>' references to declared Output names on the child template are supported.`);
31467
+ }
31468
+ async runChildDeploy(parentCtx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates) {
31469
+ const childEngine = new DeployEngine(parentCtx.stateBackend, parentCtx.lockManager, parentCtx.dagBuilder, parentCtx.diffCalculator, parentCtx.providerRegistry, {
31470
+ ...parentCtx.options ?? {},
31471
+ parameters: childParameters,
31472
+ parentStackInfo: {
31473
+ parentStack: parentCtx.parentStackName,
31474
+ parentLogicalId: logicalId,
31475
+ parentRegion: parentCtx.parentRegion
31476
+ }
31477
+ }, childRegion, parentCtx.exportIndexStore);
31478
+ await withNestedStackContext({
31479
+ ...parentCtx,
31480
+ parentStackName: childStackName,
31481
+ parentRegion: childRegion,
31482
+ nestedTemplates: grandchildTemplates
31483
+ }, () => childEngine.deploy(childStackName, childTemplate));
31484
+ }
31485
+ async readChildOutputsAsAttributes(ctx, childStackName, childRegion) {
31486
+ const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
31487
+ if (!childStateData) throw new Error(`Child stack state '${childStackName}' not found after deploy — NestedStackProvider invariant violated.`);
31488
+ return this.buildOutputsAttributes(childStateData.state.outputs ?? {});
31489
+ }
31490
+ requireContext() {
31491
+ const ctx = getCurrentNestedStackContext();
31492
+ if (!ctx) throw new Error("NestedStackProvider invoked outside withNestedStackContext() scope. The deploy / destroy CLI entry point must wrap its DeployEngine.deploy / runDestroyForStack call in withNestedStackContext(ctx, () => ...).");
31493
+ return ctx;
31494
+ }
31495
+ requireDeployContext(ctx, op) {
31496
+ if (!ctx.nestedTemplates || !ctx.dagBuilder || !ctx.diffCalculator) throw new Error(`NestedStackProvider.${op}: deploy-mode context fields (nestedTemplates / dagBuilder / diffCalculator) are missing. This usually means a destroy-mode entry point called into create/update by mistake.`);
31497
+ }
31498
+ deriveChildStackName(parentStackName, nestedLogicalId) {
31499
+ return `${parentStackName}~${nestedLogicalId}`;
31500
+ }
31501
+ synthesizeArn(accountId, region, parentStackName, logicalId) {
31502
+ return `arn:cdkd-local:${region}:${accountId}:nested-stack/${parentStackName}/${logicalId}`;
31503
+ }
31504
+ extractParameters(properties) {
31505
+ const params = properties["Parameters"];
31506
+ if (!params || typeof params !== "object" || Array.isArray(params)) return {};
31507
+ const result = {};
31508
+ for (const [k, v] of Object.entries(params)) if (typeof v === "string") result[k] = v;
31509
+ else if (typeof v === "number" || typeof v === "boolean") result[k] = String(v);
31510
+ 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]'.`);
31511
+ return result;
31512
+ }
31513
+ readChildTemplate(templatePath) {
31514
+ let raw;
31515
+ try {
31516
+ raw = fs.readFileSync(templatePath, "utf-8");
31517
+ } catch (err) {
31518
+ throw new Error(`Failed to read nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
31519
+ }
31520
+ try {
31521
+ return JSON.parse(raw);
31522
+ } catch (err) {
31523
+ throw new Error(`Failed to parse nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
31524
+ }
31525
+ }
31526
+ indexGrandchildTemplates(childTemplate, childTemplatePath) {
31527
+ const dir = path.dirname(childTemplatePath);
31528
+ const result = {};
31529
+ for (const [grandLogicalId, resource] of Object.entries(childTemplate.Resources ?? {})) {
31530
+ if (resource?.Type !== "AWS::CloudFormation::Stack") continue;
31531
+ const assetPath = resource.Metadata?.["aws:asset:path"];
31532
+ if (typeof assetPath !== "string" || assetPath.length === 0) continue;
31533
+ if (path.isAbsolute(assetPath)) throw new Error(`NestedStackProvider: nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
31534
+ result[grandLogicalId] = path.join(dir, assetPath);
31535
+ }
31536
+ return result;
31537
+ }
31538
+ buildOutputsAttributes(outputs) {
31539
+ const attributes = {};
31540
+ for (const [key, value] of Object.entries(outputs)) attributes[`Outputs.${key}`] = value;
31541
+ return attributes;
31542
+ }
31543
+ };
31544
+
30947
31545
  //#endregion
30948
31546
  //#region src/provisioning/register-providers.ts
30949
31547
  /**
@@ -31079,6 +31677,7 @@ function registerAllProviders(registry) {
31079
31677
  registry.register("AWS::S3Tables::TableBucket", s3TablesProvider);
31080
31678
  registry.register("AWS::S3Tables::Namespace", s3TablesProvider);
31081
31679
  registry.register("AWS::S3Tables::Table", s3TablesProvider);
31680
+ registry.register("AWS::CloudFormation::Stack", new NestedStackProvider());
31082
31681
  }
31083
31682
 
31084
31683
  //#endregion
@@ -31356,7 +31955,7 @@ async function deployCommand(stacks, options) {
31356
31955
  if (!await promptMigrationConfirm(pending, { yes: options.yes })) return;
31357
31956
  }
31358
31957
  }
31359
- const deployResult = await new DeployEngine(stackStateBackend, stackLockManager, dagBuilder, diffCalculator, stackProviderRegistry, {
31958
+ const deployEngineOptions = {
31360
31959
  concurrency: options.concurrency,
31361
31960
  dryRun: options.dryRun,
31362
31961
  noRollback: !options.rollback,
@@ -31365,7 +31964,23 @@ async function deployCommand(stacks, options) {
31365
31964
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
31366
31965
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
31367
31966
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
31368
- }, stackRegion, exportIndexStore).deploy(stackInfo.stackName, stackInfo.template);
31967
+ };
31968
+ const stackDeployEngine = new DeployEngine(stackStateBackend, stackLockManager, dagBuilder, diffCalculator, stackProviderRegistry, deployEngineOptions, stackRegion, exportIndexStore);
31969
+ const deployResult = await withNestedStackContext({
31970
+ stateBackend: stackStateBackend,
31971
+ lockManager: stackLockManager,
31972
+ providerRegistry: stackProviderRegistry,
31973
+ parentStackName: stackInfo.stackName,
31974
+ parentRegion: stackRegion,
31975
+ accountId,
31976
+ awsClients: stackAwsClients,
31977
+ stateBucket,
31978
+ exportIndexStore,
31979
+ nestedTemplates: stackInfo.nestedTemplates ?? {},
31980
+ dagBuilder,
31981
+ diffCalculator,
31982
+ options: deployEngineOptions
31983
+ }, () => stackDeployEngine.deploy(stackInfo.stackName, stackInfo.template));
31369
31984
  logger.info(`\n${bold("Deployment Summary:")}`);
31370
31985
  logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
31371
31986
  logger.info(` Created: ${deployResult.created > 0 ? green(deployResult.created) : gray(deployResult.created)}`);
@@ -32475,342 +33090,6 @@ function createDriftCommand() {
32475
33090
  return cmd;
32476
33091
  }
32477
33092
 
32478
- //#endregion
32479
- //#region src/cli/commands/destroy-runner.ts
32480
- /**
32481
- * Resource-type → state-property name pairs that gate AWS deletion
32482
- * protection. Used by the `--remove-protection` confirmation prompt to
32483
- * report a best-effort count of resources that will have protection
32484
- * cleared. The actual flip-off is unconditional inside each provider's
32485
- * `delete()` (idempotent — safe when AWS already has protection off),
32486
- * so the count is informational only.
32487
- *
32488
- * Most types use a boolean flag — the value `true` is what we count.
32489
- * Two types use a string-valued enum (Cognito UserPool's
32490
- * `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
32491
- * `DeletionProtection` is `'none' | 'prevent-force-deletion' |
32492
- * 'prevent-all-deletion'`). For those, the helper checks against a
32493
- * per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
32494
- *
32495
- * Exported for unit-test coverage of `countProtectedResources`.
32496
- */
32497
- const PROTECTION_PROPERTY_BY_TYPE = {
32498
- "AWS::Logs::LogGroup": "DeletionProtectionEnabled",
32499
- "AWS::RDS::DBInstance": "DeletionProtection",
32500
- "AWS::RDS::DBCluster": "DeletionProtection",
32501
- "AWS::DocDB::DBCluster": "DeletionProtection",
32502
- "AWS::Neptune::DBCluster": "DeletionProtection",
32503
- "AWS::Neptune::DBInstance": "DeletionProtection",
32504
- "AWS::DynamoDB::Table": "DeletionProtectionEnabled",
32505
- "AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
32506
- "AWS::EC2::Instance": "DisableApiTermination",
32507
- "AWS::Cognito::UserPool": "DeletionProtection",
32508
- "AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
32509
- };
32510
- /**
32511
- * For string-valued protection enums, the set of values that count as
32512
- * "currently protected". Types absent from this map use the default
32513
- * (boolean `true`).
32514
- */
32515
- const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
32516
- "AWS::Cognito::UserPool": new Set(["ACTIVE"]),
32517
- "AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
32518
- };
32519
- /**
32520
- * Count how many resources in a stack's recorded state appear to have
32521
- * deletion protection enabled. Walks `properties` and `observedProperties`
32522
- * for the property name registered against each resource type in
32523
- * `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
32524
- * `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
32525
- * handled separately via the `deletion_protection.enabled` key.
32526
- */
32527
- function countProtectedResources(state) {
32528
- let count = 0;
32529
- for (const resource of Object.values(state.resources ?? {})) {
32530
- const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
32531
- if (propName) {
32532
- const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
32533
- const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
32534
- if (activeValues) {
32535
- if (activeValues.has(recorded)) count++;
32536
- } else if (recorded === true) count++;
32537
- continue;
32538
- }
32539
- if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
32540
- if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
32541
- }
32542
- }
32543
- return count;
32544
- }
32545
- /**
32546
- * Run the destroy lifecycle for one stack against an already-loaded
32547
- * `StackState`, reusing the caller's state backend / lock manager.
32548
- *
32549
- * Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
32550
- * can call into the exact same per-stack pipeline without depending on
32551
- * synth or the CDK app. The state-source split is the only meaningful
32552
- * difference between the two commands — everything from "prompt the user"
32553
- * onwards is identical.
32554
- *
32555
- * Side effects:
32556
- * - Acquires (and releases) the stack's S3 lock.
32557
- * - Switches `process.env.AWS_REGION` for the duration of the destroy when
32558
- * the stack's recorded region differs from `baseRegion`. Restored in the
32559
- * `finally` block.
32560
- * - On full success, deletes the state file. On any failure, the state
32561
- * file is preserved so the user can retry.
32562
- */
32563
- async function runDestroyForStack(stackName, state, ctx) {
32564
- const logger = getLogger();
32565
- const result = {
32566
- stackName,
32567
- cancelled: false,
32568
- skippedEmpty: false,
32569
- deletedCount: 0,
32570
- retainedCount: 0,
32571
- errorCount: 0
32572
- };
32573
- const resourceCount = Object.keys(state.resources).length;
32574
- const regionForState = state.region ?? ctx.baseRegion;
32575
- if (resourceCount === 0) {
32576
- logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
32577
- await ctx.stateBackend.deleteState(stackName, regionForState);
32578
- logger.info(`${green("✓")} State deleted`);
32579
- result.skippedEmpty = true;
32580
- return result;
32581
- }
32582
- const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
32583
- if (needsStrongRefCheck) {
32584
- const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
32585
- if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
32586
- }
32587
- logger.info(`\nResources to be deleted (${resourceCount}):`);
32588
- for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
32589
- const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
32590
- if (!ctx.skipConfirmation) {
32591
- const rl = readline.createInterface({
32592
- input: process.stdin,
32593
- output: process.stdout
32594
- });
32595
- const prompt = ctx.removeProtection ? `\nAbout to destroy ${resourceCount} resources from stack "${stackName}", REMOVING DELETION PROTECTION on ${protectedCount} of them. Continue? (y/N): ` : `\nAre you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `;
32596
- const answer = await rl.question(prompt);
32597
- rl.close();
32598
- const trimmed = answer.trim().toLowerCase();
32599
- if (ctx.removeProtection) {
32600
- if (trimmed !== "y" && trimmed !== "yes") {
32601
- logger.info("Destroy cancelled");
32602
- result.cancelled = true;
32603
- return result;
32604
- }
32605
- } else if (trimmed === "n" || trimmed === "no") {
32606
- logger.info("Destroy cancelled");
32607
- result.cancelled = true;
32608
- return result;
32609
- }
32610
- }
32611
- const stackRegion = state.region;
32612
- let destroyProviderRegistry = ctx.providerRegistry;
32613
- let destroyAwsClients;
32614
- if (stackRegion && stackRegion !== ctx.baseRegion) {
32615
- logger.info(`Stack region: ${stackRegion}`);
32616
- process.env["AWS_REGION"] = stackRegion;
32617
- process.env["AWS_DEFAULT_REGION"] = stackRegion;
32618
- destroyAwsClients = new AwsClients({
32619
- region: stackRegion,
32620
- ...ctx.profile && { profile: ctx.profile }
32621
- });
32622
- setAwsClients(destroyAwsClients);
32623
- destroyProviderRegistry = new ProviderRegistry();
32624
- registerAllProviders(destroyProviderRegistry);
32625
- destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
32626
- }
32627
- logger.info(`\nAcquiring lock for stack ${stackName}...`);
32628
- await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
32629
- if (needsStrongRefCheck) {
32630
- const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
32631
- if (consumers.length > 0) {
32632
- try {
32633
- await ctx.lockManager.releaseLock(stackName, regionForState);
32634
- } catch (releaseErr) {
32635
- logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
32636
- }
32637
- throw new StackHasActiveImportsError(stackName, regionForState, consumers);
32638
- }
32639
- }
32640
- const renderer = getLiveRenderer();
32641
- renderer.start();
32642
- try {
32643
- logger.info("Building dependency graph...");
32644
- const template = {
32645
- AWSTemplateFormatVersion: "2010-09-09",
32646
- Resources: {}
32647
- };
32648
- for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
32649
- Type: resource.resourceType,
32650
- Properties: resource.properties || {},
32651
- ...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
32652
- };
32653
- const typeToLogicalIds = /* @__PURE__ */ new Map();
32654
- for (const [logicalId, resource] of Object.entries(state.resources)) {
32655
- const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
32656
- ids.push(logicalId);
32657
- typeToLogicalIds.set(resource.resourceType, ids);
32658
- }
32659
- for (const [logicalId, resource] of Object.entries(state.resources)) {
32660
- const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
32661
- if (!mustDeleteAfter) continue;
32662
- for (const depType of mustDeleteAfter) {
32663
- const depIds = typeToLogicalIds.get(depType);
32664
- if (!depIds) continue;
32665
- for (const depId of depIds) {
32666
- const existing = template.Resources[depId]?.DependsOn ?? [];
32667
- const depsArray = Array.isArray(existing) ? existing : [existing];
32668
- if (!depsArray.includes(logicalId)) {
32669
- template.Resources[depId] = {
32670
- ...template.Resources[depId],
32671
- DependsOn: [...depsArray, logicalId]
32672
- };
32673
- logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
32674
- }
32675
- }
32676
- }
32677
- }
32678
- const dagBuilder = new DagBuilder();
32679
- const graph = dagBuilder.buildGraph(template);
32680
- const executionLevels = dagBuilder.getExecutionLevels(graph);
32681
- logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
32682
- for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
32683
- const level = executionLevels[levelIndex];
32684
- if (!level) continue;
32685
- logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
32686
- const stackRegion = state.region ?? ctx.baseRegion;
32687
- const deletePromises = level.map(async (logicalId) => {
32688
- const resource = state.resources[logicalId];
32689
- if (!resource) {
32690
- logger.warn(`Resource ${logicalId} not found in state, skipping`);
32691
- return;
32692
- }
32693
- if (shouldRetainResource(resource.deletionPolicy)) {
32694
- logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
32695
- result.retainedCount++;
32696
- return;
32697
- }
32698
- const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
32699
- renderer.addTask(logicalId, baseLabel);
32700
- try {
32701
- const provider = destroyProviderRegistry.getProvider(resource.resourceType);
32702
- const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
32703
- const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
32704
- const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
32705
- const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
32706
- await withResourceDeadline(async () => {
32707
- const maxAttempts = provider.disableOuterRetry ? 0 : 3;
32708
- let lastDeleteError;
32709
- for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
32710
- await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
32711
- ...state.region !== void 0 && { expectedRegion: state.region },
32712
- ...ctx.removeProtection === true && { removeProtection: true }
32713
- });
32714
- lastDeleteError = null;
32715
- break;
32716
- } catch (retryError) {
32717
- lastDeleteError = retryError;
32718
- const msg = retryError instanceof Error ? retryError.message : String(retryError);
32719
- if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
32720
- const delay = 5e3 * Math.pow(2, attempt);
32721
- logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
32722
- await new Promise((resolve) => setTimeout(resolve, delay));
32723
- }
32724
- if (lastDeleteError) throw lastDeleteError;
32725
- }, {
32726
- warnAfterMs,
32727
- timeoutMs,
32728
- onWarn: (elapsedMs) => {
32729
- const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
32730
- renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
32731
- renderer.printAbove(() => {
32732
- logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
32733
- });
32734
- },
32735
- onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
32736
- });
32737
- renderer.removeTask(logicalId);
32738
- logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
32739
- result.deletedCount++;
32740
- } catch (error) {
32741
- renderer.removeTask(logicalId);
32742
- const msg = error instanceof Error ? error.message : String(error);
32743
- if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
32744
- logger.debug(` ${logicalId} already deleted, removing from state`);
32745
- result.deletedCount++;
32746
- } else if (error instanceof ResourceTimeoutError) {
32747
- const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
32748
- logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
32749
- result.errorCount++;
32750
- } else {
32751
- logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
32752
- result.errorCount++;
32753
- }
32754
- } finally {
32755
- renderer.removeTask(logicalId);
32756
- }
32757
- });
32758
- await Promise.all(deletePromises);
32759
- }
32760
- if (result.errorCount === 0) {
32761
- await ctx.stateBackend.deleteState(stackName, regionForState);
32762
- logger.debug("State deleted");
32763
- if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
32764
- } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
32765
- const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
32766
- if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
32767
- 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.`);
32768
- } finally {
32769
- renderer.stop();
32770
- logger.debug("Releasing lock...");
32771
- await ctx.lockManager.releaseLock(stackName, regionForState);
32772
- if (destroyAwsClients) {
32773
- destroyAwsClients.destroy();
32774
- process.env["AWS_REGION"] = ctx.baseRegion;
32775
- process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
32776
- setAwsClients(ctx.baseAwsClients);
32777
- }
32778
- }
32779
- return result;
32780
- }
32781
- /**
32782
- * Strong-reference scan: read every other stack's state.json from the
32783
- * state bucket and check whether any of its `imports[]` entries names
32784
- * `producerStack`. Returns the list of offending consumers (possibly
32785
- * empty).
32786
- *
32787
- * NEVER trusts the persistent exports index — a stale index could miss
32788
- * a freshly-recorded consumer and let a destructive destroy through.
32789
- * The cost is one `listStacks` + N parallel GETs at destroy time only
32790
- * (not the deploy hot path), which the user-facing UX rationalizes as
32791
- * the "destroy is slow OK" trade-off (Issue #343).
32792
- */
32793
- async function scanActiveConsumers(producerStack, producerRegion, ctx) {
32794
- const refs = await ctx.stateBackend.listStacks();
32795
- return (await Promise.all(refs.map(async (ref) => {
32796
- const region = ref.region ?? ctx.baseRegion;
32797
- if (ref.stackName === producerStack && region === producerRegion) return null;
32798
- try {
32799
- const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
32800
- if (!imports || imports.length === 0) return null;
32801
- const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
32802
- if (matches.length === 0) return null;
32803
- return matches.map((entry) => ({
32804
- consumerStack: ref.stackName,
32805
- consumerRegion: region,
32806
- exportName: entry.exportName
32807
- }));
32808
- } catch {
32809
- return null;
32810
- }
32811
- }))).filter((r) => r !== null).flat();
32812
- }
32813
-
32814
33093
  //#endregion
32815
33094
  //#region src/cli/commands/destroy.ts
32816
33095
  /**
@@ -32904,6 +33183,7 @@ async function destroyCommand(stackArgs, options) {
32904
33183
  return;
32905
33184
  }
32906
33185
  logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
33186
+ const accountId = "unknown";
32907
33187
  const stateRefsByName = /* @__PURE__ */ new Map();
32908
33188
  for (const ref of allStateRefs) {
32909
33189
  const arr = stateRefsByName.get(ref.stackName) ?? [];
@@ -32941,7 +33221,25 @@ async function destroyCommand(stackArgs, options) {
32941
33221
  logger.warn(`No state found for stack ${stackName}, skipping`);
32942
33222
  continue;
32943
33223
  }
32944
- const result = await runDestroyForStack(stackName, stateResult.state, {
33224
+ const result = await withNestedStackContext({
33225
+ stateBackend,
33226
+ lockManager,
33227
+ providerRegistry,
33228
+ parentStackName: stackName,
33229
+ parentRegion: stackTargetRegion,
33230
+ accountId,
33231
+ awsClients,
33232
+ stateBucket,
33233
+ exportIndexStore,
33234
+ destroyOptions: {
33235
+ ...options.profile && { profile: options.profile },
33236
+ ...options.removeProtection === true && { removeProtection: true },
33237
+ ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
33238
+ ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
33239
+ ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
33240
+ ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
33241
+ }
33242
+ }, () => runDestroyForStack(stackName, stateResult.state, {
32945
33243
  stateBackend,
32946
33244
  lockManager,
32947
33245
  providerRegistry,
@@ -32956,7 +33254,7 @@ async function destroyCommand(stackArgs, options) {
32956
33254
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
32957
33255
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
32958
33256
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
32959
- });
33257
+ }));
32960
33258
  totalErrors += result.errorCount;
32961
33259
  }
32962
33260
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry.`);
@@ -34690,7 +34988,25 @@ async function stateDestroyCommand(stackArgs, options) {
34690
34988
  logger.warn(`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`);
34691
34989
  continue;
34692
34990
  }
34693
- const result = await runDestroyForStack(stackName, stateResult.state, {
34991
+ const result = await withNestedStackContext({
34992
+ stateBackend: setup.stateBackend,
34993
+ lockManager: setup.lockManager,
34994
+ providerRegistry,
34995
+ parentStackName: stackName,
34996
+ parentRegion: ref.region ?? setup.region,
34997
+ accountId: "unknown",
34998
+ awsClients: setup.awsClients,
34999
+ stateBucket: setup.bucket,
35000
+ exportIndexStore: setup.exportIndexStore,
35001
+ destroyOptions: {
35002
+ ...options.profile && { profile: options.profile },
35003
+ ...options.removeProtection === true && { removeProtection: true },
35004
+ ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
35005
+ ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
35006
+ ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
35007
+ ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
35008
+ }
35009
+ }, () => runDestroyForStack(stackName, stateResult.state, {
34694
35010
  stateBackend: setup.stateBackend,
34695
35011
  lockManager: setup.lockManager,
34696
35012
  providerRegistry,
@@ -34705,7 +35021,7 @@ async function stateDestroyCommand(stackArgs, options) {
34705
35021
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
34706
35022
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
34707
35023
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
34708
- });
35024
+ }));
34709
35025
  totalErrors += result.errorCount;
34710
35026
  }
34711
35027
  }
@@ -52812,8 +53128,11 @@ function createLocalCommand() {
52812
53128
  *
52813
53129
  * - `AWS::CDK::Metadata` is a CDK sentinel; not a real AWS resource and
52814
53130
  * CFn refuses to import it.
52815
- * - `AWS::CloudFormation::Stack` is a nested stack reference; importing
52816
- * means re-creating the child stack, not adopting AWS resources.
53131
+ * - `AWS::CloudFormation::Stack` is a nested stack reference. Fresh
53132
+ * `cdkd deploy` of nested stacks IS supported (issue #459), but
53133
+ * moving an existing nested-stack hierarchy from cdkd back into
53134
+ * CloudFormation via `cdkd export` is deferred to the
53135
+ * [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
52817
53136
  * - `AWS::CloudFormation::CustomResource` is the CFn resource type CDK
52818
53137
  * emits for `new cdk.CustomResource(...)` when no `resourceType` is
52819
53138
  * passed. Functionally identical to `Custom::*` — Lambda-backed,
@@ -53270,7 +53589,10 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
53270
53589
  * `AWS::CloudFormation::Stack` (nested stacks) is intentionally NOT in
53271
53590
  * this set: CFn would CREATE a duplicate nested stack rather than adopt
53272
53591
  * the existing one, which would conflict with whatever the cdkd state
53273
- * thought it owned. cdkd doesn't deploy nested stacks anyway.
53592
+ * thought it owned. Fresh `cdkd deploy` of nested stacks is supported
53593
+ * via the recursive `NestedStackProvider` (#459), but `cdkd export`
53594
+ * adoption back into CloudFormation is deferred to the
53595
+ * [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
53274
53596
  *
53275
53597
  * Exported for unit testing.
53276
53598
  */
@@ -54222,10 +54544,14 @@ function compareSemver(a, b) {
54222
54544
  * different from the metadata-transfer migration this command
54223
54545
  * provides.
54224
54546
  *
54225
- * - `AWS::CloudFormation::Stack` — nested stacks. cdkd has no
54226
- * provider for this type, and the matching `cdk migrate` output
54227
- * flattens nested stacks into separate generated apps in a way
54228
- * that doesn't round-trip cleanly. Out of scope for #465.
54547
+ * - `AWS::CloudFormation::Stack` — nested stacks. cdkd's recursive
54548
+ * `NestedStackProvider` handles fresh `cdkd deploy` (#459), but
54549
+ * `cdk migrate` flattens nested stacks into separate generated apps
54550
+ * in a way that doesn't round-trip cleanly into cdkd's parent~child
54551
+ * state-key layout, so the migrate command keeps rejecting them.
54552
+ * Adopting an existing CFn-managed nested-stack hierarchy is
54553
+ * deferred to issue
54554
+ * [#464](https://github.com/go-to-k/cdkd/issues/464).
54229
54555
  *
54230
54556
  * - `Custom::*` — any user-defined Custom Resource type prefix.
54231
54557
  * Same rationale as `AWS::CloudFormation::CustomResource`.
@@ -55239,7 +55565,7 @@ function reorderArgs(argv) {
55239
55565
  */
55240
55566
  async function main() {
55241
55567
  const program = new Command();
55242
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.143.0");
55568
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.144.0");
55243
55569
  program.addCommand(createBootstrapCommand());
55244
55570
  program.addCommand(createSynthCommand());
55245
55571
  program.addCommand(createListCommand());