@go-to-k/cdkd 0.142.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";
@@ -30547,8 +30549,10 @@ var ASGProvider = class ASGProvider {
30547
30549
  const lt = properties["LaunchTemplate"];
30548
30550
  if (!lt) return void 0;
30549
30551
  const out = {};
30550
- if (lt.LaunchTemplateId !== void 0) out.LaunchTemplateId = lt.LaunchTemplateId;
30551
- else if (lt.LaunchTemplateName !== void 0) out.LaunchTemplateName = lt.LaunchTemplateName;
30552
+ if (lt.LaunchTemplateId !== void 0) {
30553
+ out.LaunchTemplateId = lt.LaunchTemplateId;
30554
+ if (lt.LaunchTemplateName !== void 0) this.logger.debug(`buildLaunchTemplate: both LaunchTemplateId (${lt.LaunchTemplateId}) and LaunchTemplateName (${lt.LaunchTemplateName}) templated; dropping Name (#551)`);
30555
+ } else if (lt.LaunchTemplateName !== void 0) out.LaunchTemplateName = lt.LaunchTemplateName;
30552
30556
  if (lt.Version !== void 0) out.Version = String(lt.Version);
30553
30557
  if (out.LaunchTemplateId === void 0 && out.LaunchTemplateName === void 0) return;
30554
30558
  return out;
@@ -30730,7 +30734,9 @@ var ASGProvider = class ASGProvider {
30730
30734
  if (lastObserved.size === expected.size && [...expected].every((a) => lastObserved.has(a))) return;
30731
30735
  await new Promise((r) => setTimeout(r, ASGProvider.TG_CONVERGENCE_POLL_INTERVAL_MS));
30732
30736
  }
30733
- this.logger.warn(`applyTargetGroupArnsDiff: TG set did not converge within ${ASGProvider.TG_CONVERGENCE_TIMEOUT_MS}ms for ASG ${physicalId}. expected=${JSON.stringify([...expected])} observed=${JSON.stringify([...lastObserved])}`);
30737
+ const expectedSorted = [...expected].sort();
30738
+ const observedSorted = [...lastObserved].sort();
30739
+ this.logger.warn(`applyTargetGroupArnsDiff: TG set did not converge within ${ASGProvider.TG_CONVERGENCE_TIMEOUT_MS}ms for ASG ${physicalId}. expected=${JSON.stringify(expectedSorted)} observed=${JSON.stringify(observedSorted)}`);
30734
30740
  }
30735
30741
  async applyMetricsCollectionDiff(physicalId, next, prev) {
30736
30742
  if (JSON.stringify(next ?? []) === JSON.stringify(prev ?? [])) return;
@@ -30940,6 +30946,602 @@ function mapNotificationsToCfn(configurations) {
30940
30946
  return result;
30941
30947
  }
30942
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
+
30943
31545
  //#endregion
30944
31546
  //#region src/provisioning/register-providers.ts
30945
31547
  /**
@@ -31075,6 +31677,7 @@ function registerAllProviders(registry) {
31075
31677
  registry.register("AWS::S3Tables::TableBucket", s3TablesProvider);
31076
31678
  registry.register("AWS::S3Tables::Namespace", s3TablesProvider);
31077
31679
  registry.register("AWS::S3Tables::Table", s3TablesProvider);
31680
+ registry.register("AWS::CloudFormation::Stack", new NestedStackProvider());
31078
31681
  }
31079
31682
 
31080
31683
  //#endregion
@@ -31352,7 +31955,7 @@ async function deployCommand(stacks, options) {
31352
31955
  if (!await promptMigrationConfirm(pending, { yes: options.yes })) return;
31353
31956
  }
31354
31957
  }
31355
- const deployResult = await new DeployEngine(stackStateBackend, stackLockManager, dagBuilder, diffCalculator, stackProviderRegistry, {
31958
+ const deployEngineOptions = {
31356
31959
  concurrency: options.concurrency,
31357
31960
  dryRun: options.dryRun,
31358
31961
  noRollback: !options.rollback,
@@ -31361,7 +31964,23 @@ async function deployCommand(stacks, options) {
31361
31964
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
31362
31965
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
31363
31966
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
31364
- }, 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));
31365
31984
  logger.info(`\n${bold("Deployment Summary:")}`);
31366
31985
  logger.info(` Stack: ${bold(cyan(deployResult.stackName))}`);
31367
31986
  logger.info(` Created: ${deployResult.created > 0 ? green(deployResult.created) : gray(deployResult.created)}`);
@@ -32471,342 +33090,6 @@ function createDriftCommand() {
32471
33090
  return cmd;
32472
33091
  }
32473
33092
 
32474
- //#endregion
32475
- //#region src/cli/commands/destroy-runner.ts
32476
- /**
32477
- * Resource-type → state-property name pairs that gate AWS deletion
32478
- * protection. Used by the `--remove-protection` confirmation prompt to
32479
- * report a best-effort count of resources that will have protection
32480
- * cleared. The actual flip-off is unconditional inside each provider's
32481
- * `delete()` (idempotent — safe when AWS already has protection off),
32482
- * so the count is informational only.
32483
- *
32484
- * Most types use a boolean flag — the value `true` is what we count.
32485
- * Two types use a string-valued enum (Cognito UserPool's
32486
- * `DeletionProtection` is `'ACTIVE' | 'INACTIVE'`, AutoScalingGroup's
32487
- * `DeletionProtection` is `'none' | 'prevent-force-deletion' |
32488
- * 'prevent-all-deletion'`). For those, the helper checks against a
32489
- * per-type set of "active" values via `PROTECTION_ACTIVE_VALUES_BY_TYPE`.
32490
- *
32491
- * Exported for unit-test coverage of `countProtectedResources`.
32492
- */
32493
- const PROTECTION_PROPERTY_BY_TYPE = {
32494
- "AWS::Logs::LogGroup": "DeletionProtectionEnabled",
32495
- "AWS::RDS::DBInstance": "DeletionProtection",
32496
- "AWS::RDS::DBCluster": "DeletionProtection",
32497
- "AWS::DocDB::DBCluster": "DeletionProtection",
32498
- "AWS::Neptune::DBCluster": "DeletionProtection",
32499
- "AWS::Neptune::DBInstance": "DeletionProtection",
32500
- "AWS::DynamoDB::Table": "DeletionProtectionEnabled",
32501
- "AWS::DynamoDB::GlobalTable": "DeletionProtectionEnabled",
32502
- "AWS::EC2::Instance": "DisableApiTermination",
32503
- "AWS::Cognito::UserPool": "DeletionProtection",
32504
- "AWS::AutoScaling::AutoScalingGroup": "DeletionProtection"
32505
- };
32506
- /**
32507
- * For string-valued protection enums, the set of values that count as
32508
- * "currently protected". Types absent from this map use the default
32509
- * (boolean `true`).
32510
- */
32511
- const PROTECTION_ACTIVE_VALUES_BY_TYPE = {
32512
- "AWS::Cognito::UserPool": new Set(["ACTIVE"]),
32513
- "AWS::AutoScaling::AutoScalingGroup": new Set(["prevent-force-deletion", "prevent-all-deletion"])
32514
- };
32515
- /**
32516
- * Count how many resources in a stack's recorded state appear to have
32517
- * deletion protection enabled. Walks `properties` and `observedProperties`
32518
- * for the property name registered against each resource type in
32519
- * `PROTECTION_PROPERTY_BY_TYPE`. ELBv2 LoadBalancer protection lives in
32520
- * `LoadBalancerAttributes` (a CFn `Array<{Key, Value}>`), so it's
32521
- * handled separately via the `deletion_protection.enabled` key.
32522
- */
32523
- function countProtectedResources(state) {
32524
- let count = 0;
32525
- for (const resource of Object.values(state.resources ?? {})) {
32526
- const propName = PROTECTION_PROPERTY_BY_TYPE[resource.resourceType];
32527
- if (propName) {
32528
- const recorded = resource.properties?.[propName] ?? resource.observedProperties?.[propName];
32529
- const activeValues = PROTECTION_ACTIVE_VALUES_BY_TYPE[resource.resourceType];
32530
- if (activeValues) {
32531
- if (activeValues.has(recorded)) count++;
32532
- } else if (recorded === true) count++;
32533
- continue;
32534
- }
32535
- if (resource.resourceType === "AWS::ElasticLoadBalancingV2::LoadBalancer") {
32536
- if (((resource.properties?.["LoadBalancerAttributes"] ?? resource.observedProperties?.["LoadBalancerAttributes"])?.find((a) => a?.Key === "deletion_protection.enabled"))?.Value === "true") count++;
32537
- }
32538
- }
32539
- return count;
32540
- }
32541
- /**
32542
- * Run the destroy lifecycle for one stack against an already-loaded
32543
- * `StackState`, reusing the caller's state backend / lock manager.
32544
- *
32545
- * Hoisted from `cdkd destroy` so the new `cdkd state destroy` subcommand
32546
- * can call into the exact same per-stack pipeline without depending on
32547
- * synth or the CDK app. The state-source split is the only meaningful
32548
- * difference between the two commands — everything from "prompt the user"
32549
- * onwards is identical.
32550
- *
32551
- * Side effects:
32552
- * - Acquires (and releases) the stack's S3 lock.
32553
- * - Switches `process.env.AWS_REGION` for the duration of the destroy when
32554
- * the stack's recorded region differs from `baseRegion`. Restored in the
32555
- * `finally` block.
32556
- * - On full success, deletes the state file. On any failure, the state
32557
- * file is preserved so the user can retry.
32558
- */
32559
- async function runDestroyForStack(stackName, state, ctx) {
32560
- const logger = getLogger();
32561
- const result = {
32562
- stackName,
32563
- cancelled: false,
32564
- skippedEmpty: false,
32565
- deletedCount: 0,
32566
- retainedCount: 0,
32567
- errorCount: 0
32568
- };
32569
- const resourceCount = Object.keys(state.resources).length;
32570
- const regionForState = state.region ?? ctx.baseRegion;
32571
- if (resourceCount === 0) {
32572
- logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
32573
- await ctx.stateBackend.deleteState(stackName, regionForState);
32574
- logger.info(`${green("✓")} State deleted`);
32575
- result.skippedEmpty = true;
32576
- return result;
32577
- }
32578
- const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
32579
- if (needsStrongRefCheck) {
32580
- const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
32581
- if (consumers.length > 0) throw new StackHasActiveImportsError(stackName, regionForState, consumers);
32582
- }
32583
- logger.info(`\nResources to be deleted (${resourceCount}):`);
32584
- for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
32585
- const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
32586
- if (!ctx.skipConfirmation) {
32587
- const rl = readline.createInterface({
32588
- input: process.stdin,
32589
- output: process.stdout
32590
- });
32591
- 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): `;
32592
- const answer = await rl.question(prompt);
32593
- rl.close();
32594
- const trimmed = answer.trim().toLowerCase();
32595
- if (ctx.removeProtection) {
32596
- if (trimmed !== "y" && trimmed !== "yes") {
32597
- logger.info("Destroy cancelled");
32598
- result.cancelled = true;
32599
- return result;
32600
- }
32601
- } else if (trimmed === "n" || trimmed === "no") {
32602
- logger.info("Destroy cancelled");
32603
- result.cancelled = true;
32604
- return result;
32605
- }
32606
- }
32607
- const stackRegion = state.region;
32608
- let destroyProviderRegistry = ctx.providerRegistry;
32609
- let destroyAwsClients;
32610
- if (stackRegion && stackRegion !== ctx.baseRegion) {
32611
- logger.info(`Stack region: ${stackRegion}`);
32612
- process.env["AWS_REGION"] = stackRegion;
32613
- process.env["AWS_DEFAULT_REGION"] = stackRegion;
32614
- destroyAwsClients = new AwsClients({
32615
- region: stackRegion,
32616
- ...ctx.profile && { profile: ctx.profile }
32617
- });
32618
- setAwsClients(destroyAwsClients);
32619
- destroyProviderRegistry = new ProviderRegistry();
32620
- registerAllProviders(destroyProviderRegistry);
32621
- destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
32622
- }
32623
- logger.info(`\nAcquiring lock for stack ${stackName}...`);
32624
- await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
32625
- if (needsStrongRefCheck) {
32626
- const consumers = await scanActiveConsumers(stackName, regionForState, ctx);
32627
- if (consumers.length > 0) {
32628
- try {
32629
- await ctx.lockManager.releaseLock(stackName, regionForState);
32630
- } catch (releaseErr) {
32631
- logger.warn(`Failed to release lock after strong-ref refusal: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
32632
- }
32633
- throw new StackHasActiveImportsError(stackName, regionForState, consumers);
32634
- }
32635
- }
32636
- const renderer = getLiveRenderer();
32637
- renderer.start();
32638
- try {
32639
- logger.info("Building dependency graph...");
32640
- const template = {
32641
- AWSTemplateFormatVersion: "2010-09-09",
32642
- Resources: {}
32643
- };
32644
- for (const [logicalId, resource] of Object.entries(state.resources)) template.Resources[logicalId] = {
32645
- Type: resource.resourceType,
32646
- Properties: resource.properties || {},
32647
- ...resource.dependencies && resource.dependencies.length > 0 && { DependsOn: resource.dependencies }
32648
- };
32649
- const typeToLogicalIds = /* @__PURE__ */ new Map();
32650
- for (const [logicalId, resource] of Object.entries(state.resources)) {
32651
- const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
32652
- ids.push(logicalId);
32653
- typeToLogicalIds.set(resource.resourceType, ids);
32654
- }
32655
- for (const [logicalId, resource] of Object.entries(state.resources)) {
32656
- const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
32657
- if (!mustDeleteAfter) continue;
32658
- for (const depType of mustDeleteAfter) {
32659
- const depIds = typeToLogicalIds.get(depType);
32660
- if (!depIds) continue;
32661
- for (const depId of depIds) {
32662
- const existing = template.Resources[depId]?.DependsOn ?? [];
32663
- const depsArray = Array.isArray(existing) ? existing : [existing];
32664
- if (!depsArray.includes(logicalId)) {
32665
- template.Resources[depId] = {
32666
- ...template.Resources[depId],
32667
- DependsOn: [...depsArray, logicalId]
32668
- };
32669
- logger.debug(`Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`);
32670
- }
32671
- }
32672
- }
32673
- }
32674
- const dagBuilder = new DagBuilder();
32675
- const graph = dagBuilder.buildGraph(template);
32676
- const executionLevels = dagBuilder.getExecutionLevels(graph);
32677
- logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
32678
- for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
32679
- const level = executionLevels[levelIndex];
32680
- if (!level) continue;
32681
- logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
32682
- const stackRegion = state.region ?? ctx.baseRegion;
32683
- const deletePromises = level.map(async (logicalId) => {
32684
- const resource = state.resources[logicalId];
32685
- if (!resource) {
32686
- logger.warn(`Resource ${logicalId} not found in state, skipping`);
32687
- return;
32688
- }
32689
- if (shouldRetainResource(resource.deletionPolicy)) {
32690
- logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
32691
- result.retainedCount++;
32692
- return;
32693
- }
32694
- const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
32695
- renderer.addTask(logicalId, baseLabel);
32696
- try {
32697
- const provider = destroyProviderRegistry.getProvider(resource.resourceType);
32698
- const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
32699
- const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? 3e5;
32700
- const globalTimeoutMs = ctx.resourceTimeoutMs ?? 18e5;
32701
- const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
32702
- await withResourceDeadline(async () => {
32703
- const maxAttempts = provider.disableOuterRetry ? 0 : 3;
32704
- let lastDeleteError;
32705
- for (let attempt = 0; attempt <= maxAttempts; attempt++) try {
32706
- await provider.delete(logicalId, resource.physicalId, resource.resourceType, resource.properties, {
32707
- ...state.region !== void 0 && { expectedRegion: state.region },
32708
- ...ctx.removeProtection === true && { removeProtection: true }
32709
- });
32710
- lastDeleteError = null;
32711
- break;
32712
- } catch (retryError) {
32713
- lastDeleteError = retryError;
32714
- const msg = retryError instanceof Error ? retryError.message : String(retryError);
32715
- if (!(msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation")) || attempt >= maxAttempts) break;
32716
- const delay = 5e3 * Math.pow(2, attempt);
32717
- logger.debug(` ⏳ Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`);
32718
- await new Promise((resolve) => setTimeout(resolve, delay));
32719
- }
32720
- if (lastDeleteError) throw lastDeleteError;
32721
- }, {
32722
- warnAfterMs,
32723
- timeoutMs,
32724
- onWarn: (elapsedMs) => {
32725
- const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
32726
- renderer.updateTaskLabel(logicalId, `${baseLabel} [taking longer than expected, ${minutes}m+]`);
32727
- renderer.printAbove(() => {
32728
- logger.warn(`${logicalId} (${resource.resourceType}) has been deleting for ${minutes}m — still waiting`);
32729
- });
32730
- },
32731
- onTimeout: (elapsedMs) => new ResourceTimeoutError(logicalId, resource.resourceType, stackRegion, elapsedMs, "DELETE", timeoutMs)
32732
- });
32733
- renderer.removeTask(logicalId);
32734
- logger.info(` ${red("✗")} ${bold(logicalId)} ${gray(`(${resource.resourceType})`)} ${red("deleted")}`);
32735
- result.deletedCount++;
32736
- } catch (error) {
32737
- renderer.removeTask(logicalId);
32738
- const msg = error instanceof Error ? error.message : String(error);
32739
- if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
32740
- logger.debug(` ${logicalId} already deleted, removing from state`);
32741
- result.deletedCount++;
32742
- } else if (error instanceof ResourceTimeoutError) {
32743
- const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
32744
- logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
32745
- result.errorCount++;
32746
- } else {
32747
- logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
32748
- result.errorCount++;
32749
- }
32750
- } finally {
32751
- renderer.removeTask(logicalId);
32752
- }
32753
- });
32754
- await Promise.all(deletePromises);
32755
- }
32756
- if (result.errorCount === 0) {
32757
- await ctx.stateBackend.deleteState(stackName, regionForState);
32758
- logger.debug("State deleted");
32759
- if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
32760
- } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
32761
- const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
32762
- if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
32763
- 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.`);
32764
- } finally {
32765
- renderer.stop();
32766
- logger.debug("Releasing lock...");
32767
- await ctx.lockManager.releaseLock(stackName, regionForState);
32768
- if (destroyAwsClients) {
32769
- destroyAwsClients.destroy();
32770
- process.env["AWS_REGION"] = ctx.baseRegion;
32771
- process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
32772
- setAwsClients(ctx.baseAwsClients);
32773
- }
32774
- }
32775
- return result;
32776
- }
32777
- /**
32778
- * Strong-reference scan: read every other stack's state.json from the
32779
- * state bucket and check whether any of its `imports[]` entries names
32780
- * `producerStack`. Returns the list of offending consumers (possibly
32781
- * empty).
32782
- *
32783
- * NEVER trusts the persistent exports index — a stale index could miss
32784
- * a freshly-recorded consumer and let a destructive destroy through.
32785
- * The cost is one `listStacks` + N parallel GETs at destroy time only
32786
- * (not the deploy hot path), which the user-facing UX rationalizes as
32787
- * the "destroy is slow OK" trade-off (Issue #343).
32788
- */
32789
- async function scanActiveConsumers(producerStack, producerRegion, ctx) {
32790
- const refs = await ctx.stateBackend.listStacks();
32791
- return (await Promise.all(refs.map(async (ref) => {
32792
- const region = ref.region ?? ctx.baseRegion;
32793
- if (ref.stackName === producerStack && region === producerRegion) return null;
32794
- try {
32795
- const imports = (await ctx.stateBackend.getState(ref.stackName, region))?.state.imports;
32796
- if (!imports || imports.length === 0) return null;
32797
- const matches = imports.filter((entry) => entry.sourceStack === producerStack && entry.sourceRegion === producerRegion);
32798
- if (matches.length === 0) return null;
32799
- return matches.map((entry) => ({
32800
- consumerStack: ref.stackName,
32801
- consumerRegion: region,
32802
- exportName: entry.exportName
32803
- }));
32804
- } catch {
32805
- return null;
32806
- }
32807
- }))).filter((r) => r !== null).flat();
32808
- }
32809
-
32810
33093
  //#endregion
32811
33094
  //#region src/cli/commands/destroy.ts
32812
33095
  /**
@@ -32900,6 +33183,7 @@ async function destroyCommand(stackArgs, options) {
32900
33183
  return;
32901
33184
  }
32902
33185
  logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
33186
+ const accountId = "unknown";
32903
33187
  const stateRefsByName = /* @__PURE__ */ new Map();
32904
33188
  for (const ref of allStateRefs) {
32905
33189
  const arr = stateRefsByName.get(ref.stackName) ?? [];
@@ -32937,7 +33221,25 @@ async function destroyCommand(stackArgs, options) {
32937
33221
  logger.warn(`No state found for stack ${stackName}, skipping`);
32938
33222
  continue;
32939
33223
  }
32940
- 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, {
32941
33243
  stateBackend,
32942
33244
  lockManager,
32943
33245
  providerRegistry,
@@ -32952,7 +33254,7 @@ async function destroyCommand(stackArgs, options) {
32952
33254
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
32953
33255
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
32954
33256
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
32955
- });
33257
+ }));
32956
33258
  totalErrors += result.errorCount;
32957
33259
  }
32958
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.`);
@@ -34686,7 +34988,25 @@ async function stateDestroyCommand(stackArgs, options) {
34686
34988
  logger.warn(`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`);
34687
34989
  continue;
34688
34990
  }
34689
- 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, {
34690
35010
  stateBackend: setup.stateBackend,
34691
35011
  lockManager: setup.lockManager,
34692
35012
  providerRegistry,
@@ -34701,7 +35021,7 @@ async function stateDestroyCommand(stackArgs, options) {
34701
35021
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
34702
35022
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
34703
35023
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
34704
- });
35024
+ }));
34705
35025
  totalErrors += result.errorCount;
34706
35026
  }
34707
35027
  }
@@ -52808,8 +53128,11 @@ function createLocalCommand() {
52808
53128
  *
52809
53129
  * - `AWS::CDK::Metadata` is a CDK sentinel; not a real AWS resource and
52810
53130
  * CFn refuses to import it.
52811
- * - `AWS::CloudFormation::Stack` is a nested stack reference; importing
52812
- * 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.
52813
53136
  * - `AWS::CloudFormation::CustomResource` is the CFn resource type CDK
52814
53137
  * emits for `new cdk.CustomResource(...)` when no `resourceType` is
52815
53138
  * passed. Functionally identical to `Custom::*` — Lambda-backed,
@@ -53266,7 +53589,10 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
53266
53589
  * `AWS::CloudFormation::Stack` (nested stacks) is intentionally NOT in
53267
53590
  * this set: CFn would CREATE a duplicate nested stack rather than adopt
53268
53591
  * the existing one, which would conflict with whatever the cdkd state
53269
- * 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.
53270
53596
  *
53271
53597
  * Exported for unit testing.
53272
53598
  */
@@ -54218,10 +54544,14 @@ function compareSemver(a, b) {
54218
54544
  * different from the metadata-transfer migration this command
54219
54545
  * provides.
54220
54546
  *
54221
- * - `AWS::CloudFormation::Stack` — nested stacks. cdkd has no
54222
- * provider for this type, and the matching `cdk migrate` output
54223
- * flattens nested stacks into separate generated apps in a way
54224
- * 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).
54225
54555
  *
54226
54556
  * - `Custom::*` — any user-defined Custom Resource type prefix.
54227
54557
  * Same rationale as `AWS::CloudFormation::CustomResource`.
@@ -55235,7 +55565,7 @@ function reorderArgs(argv) {
55235
55565
  */
55236
55566
  async function main() {
55237
55567
  const program = new Command();
55238
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.142.0");
55568
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.144.0");
55239
55569
  program.addCommand(createBootstrapCommand());
55240
55570
  program.addCommand(createSynthCommand());
55241
55571
  program.addCommand(createListCommand());