@go-to-k/cdkd 0.286.2 → 0.286.4

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.
@@ -1,8 +1,8 @@
1
- import { $ as beginCommandInterruptScope, $n as resolveAutoAssetStorage, $r as isTransientServerError, $t as maskSecretsInError, A as formatResourceLine, An as ensureAssetStorage, At as requireConfigString, B as isExportAliasCollision, Bn as dockerSpawnEnvWithSensitive, Br as ResourceTimeoutError, Bt as INTRINSIC_KEYS, C as unsupportedFinalSnapshotError, Cn as loadPublishableAssetManifest, Cr as getAwsClients, Ct as coerceCfnBoolean, D as isStatefulRecreateTargetForReplace, Dn as AssetModeResolver, Dr as CdkdError, Dt as replayWarn, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as stripControlChars, Et as readConfigString, F as red, Fn as validateAssetBucketName, Fr as LocalStartServiceError, Ft as s3BucketDualStackDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as runDockerStreaming, Gt as STATE_SOURCED_CROSS_GENERATION_RULES, H as stateKeySecretExposure, Hn as getDockerCmd, Hr as StackHasActiveImportsError, Ht as withRetry, I as yellow, In as validateContainerRepoName, It as s3BucketRegionalDomainName, J as findActionableSilentDrops, Jn as Synthesizer, Jr as normalizeAwsError, Jt as carriesSecretMask, K as clearOnUpdateRemoval, Kn as AssetManifestLoader, Kt as STATE_SOURCED_READBACK_RULES, L as collectDeclaredOutputNames, Ln as buildDenyExternalAccessPolicy, Lr as NestedStackChildDirectDestroyError, Lt as s3BucketWebsiteUrl, M as cyan, Mn as isCrossRegionRedirect, Mr as DynamicReferenceRegionAmbiguousError, Mt as producerRegionsFromState, N as gray, Nn as parseBootstrapMarker, Nr as IntrinsicResolutionRefusalError, Nt as s3BucketArn, O as isStatefulRecreateTargetSync, On as BOOTSTRAP_MARKER_PREFIX, Ot as requireConfigArray, P as green, Pn as readBootstrapMarkerBody, Pr as LocalInvokeBuildError$1, Pt as s3BucketDomainName, Q as maskerOrIdentity, Qn as resolveApp, Qr as isThrottlingError, Qt as isSingleDynamicReferenceToken, R as collectPublishedOutputNames, Rn as describeAwsFailure, Rr as PartialFailureError, Rt as applyRoleArnIfSet, S as refusesFinalSnapshot, Sn as createAssetRedirectResolver, Sr as AwsClients, St as assertRegionMatch, T as extractDeploymentEventError, Tn as escapeRegExp, Tr as setAwsClients, Tt as configStringRefusal, U as getCurrentResourceSecrets, Un as partitionSensitiveEnv, Ur as StackTerminationProtectionError, Ut as DagBuilder, V as secretBearingStateKeyWarning, Vn as formatDockerLoginError, Vr as ResourceUpdateNotSupportedError, Vt as describeTypeWithThrottleRetry, W as IAMRoleProvider, Wn as runDockerForeground, Wt as TemplateParser, X as createMaskedRetryLogger, Xn as getDefaultStateBucketName, Xr as isMarkedNonRetryable, Xt as dynamicReferenceTokens, Y as findSilentDropProperties, Yn as synthesisStatusMessage, Yr as withErrorHandling, Yt as createSecretMasker, Z as maskDeep, Zn as getLegacyStateBucketName, Zr as isRetryableTransientError, Zt as errorCauseChain, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as shouldRetainResource, _r as derivePartitionAndUrlSuffix, _t as parameterTypeMayLoseSecretIdentity, a as DeploymentEventsStore, an as LockManager, ar as stateBucketExistenceConfirmed, at as slowCcOperationTimeoutMs, b as createPreDeleteFinalSnapshot, bn as WorkGraph, bt as normalizeAwsTagsToCfn, c as replayFailedOperations, cn as UNRENDERABLE, cr as CFN_TEMPLATE_URL_LIMIT, ct as deleteSkipReason, d as updatePartialReason, dn as forceQuitRecoveryClause, dr as uploadCfnTemplate, dt as IntrinsicFunctionResolver, ei as markNonRetryable, en as maskSecretsInText, er as resolveCaptureObservedState, et as endCommandInterruptScope, f as withResourceDeadline, fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, fr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, ft as carriesDynamicReference, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as importableOutputs, gr as canonicalizeRegion, gt as isUnboundTemplateParameter, h as computeImplicitDeleteEdges, hn as importableOutputKeys, hr as PARTITION_TABLE, ht as getAccountInfo, i as DeploymentEventsReader, in as scrubResourceRecord, ir as resolveUseCdkBootstrapAssets, it as CloudControlProvider, j as bold, jn as getBootstrapMarkerKey, jr as DeployCancelledError, jt as classifyReplaySecretRegion, k as renderStatefulReason, kn as assertAssetBucketRegion, kr as CrossAccountSecretRefusalError, kt as requireConfigObject, l as replayRollback, ln as buildForceUnlockCommand, lr as MIGRATE_TMP_PREFIX, lt as disableInstanceApiTermination, m as IMPLICIT_DELETE_DEPENDENCIES, mn as exportNamesCarriedFrom, mr as expectedOwnerParam, mt as coerceParameterTypedValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as retryClassificationText, nn as recoverMaskedOutput, nr as resolveStateBucketWithDefault, nt as isInterruptedWaitError, o as planFailedOps, on as S3StateBackend, or as warnDeprecatedNoPrefixCliFlag, ot as UNSPECIFIED_SKIP_REASON, p as maskingRetryLogger, pn as DEFAULT_STATE_PREFIX, pr as displaySafe, pt as cfnRefValueFromPhysicalId, q as ProviderRegistry, qn as getDockerImageBySourceHash, qt as TEMPLATE_SOURCED_RULES, r as DeployEngine, rn as redactSecretsForState, rr as resolveStateBucketWithDefaultAndSource, rt as startInterruptWatch, s as planRollback, sn as rebuildClientForBucketRegion, sr as CFN_TEMPLATE_BODY_LIMIT, st as deleteIndeterminateGuards, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as markRedactedCause, tn as recordMaskOnlyValue, tr as resolveSkipPrefix, tt as interruptWatchListenerCount, u as updatePartialMessage, un as buildLockContentionMessage, ur as findLargeInlineResources, ut as isTerminationProtectionPropagationError, v as buildFinalSnapshotIdentifier, vn as AssetPublisher, vr as AssemblyReader, vt as refStateLookupFromResource, w as makeCanonicalizePropertiesFn, wn as rewriteTemplateAssetReferences, wr as resetAwsClients, wt as configBooleanRefusal, x as isFinalSnapshotError, xn as buildAssetRedirectMap, xr as resolveBucketRegion, xt as resolveExplicitPhysicalId, y as ccRoutedFinalSnapshotError, yn as stringifyValue, yr as processStackMessages, yt as WAFv2WebACLProvider, z as exportAliasCollisionScrubWarning, zn as buildDockerImage, zr as ProvisioningError, zt as DiffCalculator } from "./deploy-engine-BoSlW08T.js";
1
+ import { $ as findSilentDropProperties, $n as escapeRegExp, $t as createSecretMasker, A as WARM_THROUGHPUT_MEMBERS, An as AssetModeResolver, Ar as resolveBucketRegion, At as readConfigString, B as yellow, Bn as buildDenyExternalAccessPolicy, Br as DeployCancelledError, Bt as s3BucketRegionalDomainName, C as unsupportedFinalSnapshotError, Cn as AssetPublisher, Cr as expectedOwnerParam, Ct as WAFv2WebACLProvider, D as isStatefulRecreateTargetForReplace, Dn as createAssetRedirectResolver, Dr as AssemblyReader, Dt as coerceCfnBoolean, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as buildAssetRedirectMap, Er as derivePartitionAndUrlSuffix, Et as assertRegionMatch, F as bold, Fn as isCrossRegionRedirect, Ft as classifyReplaySecretRegion, G as secretBearingStateKeyWarning, Gn as describeDockerFailure, Gt as describeTypeWithThrottleRetry, H as collectPublishedOutputNames, Hn as buildDockerImage, Hr as IntrinsicResolutionRefusalError, Ht as applyRoleArnIfSet, I as cyan, In as parseBootstrapMarker, Ir as CdkdError, It as producerRegionsFromState, J as IAMRoleProvider, Jn as getDockerCmd, Jr as ProvisioningError, Jt as TemplateParser, K as stateKeySecretExposure, Kn as dockerSpawnEnvWithSensitive, Kr as NestedStackChildDirectDestroyError, Kt as withRetry, L as gray, Ln as readBootstrapMarkerBody, Lt as s3BucketArn, M as isWarmThroughputDecrease, Mn as assertAssetBucketRegion, Mr as getAwsClients, Mt as requireConfigArray, N as toFiniteNumber, Nn as ensureAssetStorage, Nr as resetAwsClients, Nt as requireConfigObject, O as isStatefulRecreateTargetSync, On as loadPublishableAssetManifest, Or as processStackMessages, Ot as configBooleanRefusal, P as formatResourceLine, Pn as getBootstrapMarkerKey, Pr as setAwsClients, Pt as requireConfigString, Q as findActionableSilentDrops, Qn as runDockerStreaming, Qr as StackTerminationProtectionError, Qt as carriesSecretMask, R as green, Rn as validateAssetBucketName, Rr as CrossAccountSecretRefusalError, Rt as s3BucketDomainName, S as refusesFinalSnapshot, Sn as shouldRetainResource, Sr as displaySafe, St as refStateLookupFromResource, T as extractDeploymentEventError, Tn as WorkGraph, Tr as canonicalizeRegion, Tt as resolveExplicitPhysicalId, U as exportAliasCollisionScrubWarning, Un as describeDockerCapturedOutput, Ur as LocalInvokeBuildError$1, Ut as DiffCalculator, V as collectDeclaredOutputNames, Vn as describeAwsFailure, Vr as DynamicReferenceRegionAmbiguousError, Vt as s3BucketWebsiteUrl, W as isExportAliasCollision, Wn as describeDockerExecFailure, Wr as LocalStartServiceError, Wt as INTRINSIC_KEYS, X as clearOnUpdateRemoval, Xn as redactDockerArgvValues, Xr as ResourceUpdateNotSupportedError, Xt as STATE_SOURCED_READBACK_RULES, Y as collectInlinePolicyNamesManagedBySiblings, Yn as partitionSensitiveEnv, Yr as ResourceTimeoutError, Yt as STATE_SOURCED_CROSS_GENERATION_RULES, Z as ProviderRegistry, Zn as runDockerForeground, Zr as StackHasActiveImportsError, Zt as TEMPLATE_SOURCED_RULES, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as CUSTOM_RESOURCE_RESPONSE_PREFIX, _r as CFN_TEMPLATE_URL_LIMIT, _t as cfnRefValueFromPhysicalId, a as DeploymentEventsStore, ai as isMarkedNonRetryable, an as recordMaskOnlyValue, ar as getDefaultStateBucketName, at as interruptWatchListenerCount, b as createPreDeleteFinalSnapshot, bn as importableOutputKeys, br as uploadCfnTemplate, bt as isUnboundTemplateParameter, c as replayFailedOperations, ci as isTransientServerError, cn as scrubResourceRecord, cr as resolveAutoAssetStorage, ct as CloudControlProvider, d as updatePartialReason, di as retryClassificationText, dn as rebuildClientForBucketRegion, dr as resolveStateBucketWithDefault, dt as deleteIndeterminateGuards, en as dynamicReferenceTokens, er as stripControlChars, et as createMaskedRetryLogger, f as withResourceDeadline, fn as UNRENDERABLE, fr as resolveStateBucketWithDefaultAndSource, ft as deleteSkipReason, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as shellQuote, gr as CFN_TEMPLATE_BODY_LIMIT, gt as carriesDynamicReference, h as computeImplicitDeleteEdges, hn as forceQuitRecoveryClause, hr as warnDeprecatedNoPrefixCliFlag, ht as IntrinsicFunctionResolver, i as DeploymentEventsReader, ii as withErrorHandling, in as maskSecretsInText, ir as synthesisStatusMessage, it as endCommandInterruptScope, j as coerceWarmThroughput$1, jn as BOOTSTRAP_MARKER_PREFIX, jr as AwsClients, jt as replayWarn, k as renderStatefulReason, kn as rewriteTemplateAssetReferences, kt as configStringRefusal, l as replayRollback, li as markNonRetryable, ln as LockManager, lr as resolveCaptureObservedState, lt as slowCcOperationTimeoutMs, m as IMPLICIT_DELETE_DEPENDENCIES, mn as buildLockContentionMessage, mr as stateBucketExistenceConfirmed, mt as isTerminationProtectionPropagationError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as isSingleDynamicReferenceToken, nr as getDockerImageBySourceHash, nt as maskerOrIdentity, o as planFailedOps, oi as isRetryableTransientError, on as recoverMaskedOutput, or as getLegacyStateBucketName, ot as isInterruptedWaitError, p as maskingRetryLogger, pn as buildForceUnlockCommand, pr as resolveUseCdkBootstrapAssets, pt as disableInstanceApiTermination, q as getCurrentResourceSecrets, qn as formatDockerLoginError, qr as PartialFailureError, qt as DagBuilder, r as DeployEngine, ri as normalizeAwsError, rn as maskSecretsInError, rr as Synthesizer, rt as beginCommandInterruptScope, s as planRollback, si as isThrottlingError, sn as redactSecretsForState, sr as resolveApp, st as startInterruptWatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as errorCauseChain, tr as AssetManifestLoader, tt as maskDeep, u as updatePartialMessage, ui as markRedactedCause, un as S3StateBackend, ur as resolveSkipPrefix, ut as UNSPECIFIED_SKIP_REASON, v as buildFinalSnapshotIdentifier, vn as DEFAULT_STATE_PREFIX, vr as MIGRATE_TMP_PREFIX, vt as coerceParameterTypedValue, w as makeCanonicalizePropertiesFn, wn as stringifyValue, wr as PARTITION_TABLE, wt as normalizeAwsTagsToCfn, x as isFinalSnapshotError, xn as importableOutputs, xr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, xt as parameterTypeMayLoseSecretIdentity, y as ccRoutedFinalSnapshotError, yn as exportNamesCarriedFrom, yr as findLargeInlineResources, yt as getAccountInfo, z as red, zn as validateContainerRepoName, zt as s3BucketDualStackDomainName } from "./deploy-engine-DylsbAdu.js";
2
2
  import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-Crg_SdRh.js";
5
- import { t as ASGProvider } from "./asg-provider-CSEPihOt.js";
4
+ import { t as getCdkdVersion } from "./version-CKOnsgg1.js";
5
+ import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-dBSdt3nB.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
8
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, NotFound, 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";
@@ -36,6 +36,7 @@ import { AddTagsCommand, CreateListenerCommand, CreateLoadBalancerCommand, Creat
36
36
  import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$11, UntagResourceCommand as UntagResourceCommand$11, UpdateAliasCommand, UpdateKeyDescriptionCommand } from "@aws-sdk/client-kms";
37
37
  import { join as join$1 } from "path";
38
38
  import { BatchDeleteImageCommand, CreateRepositoryCommand, DeleteLifecyclePolicyCommand, DeleteRepositoryCommand, DeleteRepositoryPolicyCommand, DescribeImagesCommand as DescribeImagesCommand$1, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, GetLifecyclePolicyCommand, LifecyclePolicyNotFoundException, ListTagsForResourceCommand as ListTagsForResourceCommand$9, PutImageScanningConfigurationCommand, PutImageTagMutabilityCommand, PutLifecyclePolicyCommand, RepositoryNotEmptyException, RepositoryNotFoundException, SetRepositoryPolicyCommand, TagResourceCommand as TagResourceCommand$12, UntagResourceCommand as UntagResourceCommand$12 } from "@aws-sdk/client-ecr";
39
+ import { isDeepStrictEqual, promisify } from "node:util";
39
40
  import graphlib from "graphlib";
40
41
  import { AddTagsToResourceCommand as AddTagsToResourceCommand$2, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBProxyCommand, CreateDBProxyEndpointCommand, CreateDBSubnetGroupCommand, DBProxyEndpointNotFoundFault, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBProxyCommand, DeleteDBProxyEndpointCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxiesCommand, DescribeDBProxyEndpointsCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBProxyTargetsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$10, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyCommand, ModifyDBProxyEndpointCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$2 } from "@aws-sdk/client-rds";
41
42
  import { Command, InvalidArgumentError, Option } from "commander";
@@ -45,7 +46,6 @@ import { writeFileSync as writeFileSync$1 } from "fs";
45
46
  import { Document, Pair, Scalar, YAMLMap, YAMLSeq, isScalar, parse, stringify, visit } from "yaml";
46
47
  import * as zlib from "node:zlib";
47
48
  import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
48
- import { isDeepStrictEqual, promisify } from "node:util";
49
49
  import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteAccessLogSettingsCommand, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteCorsConfigurationCommand, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteRouteRequestParameterCommand, DeleteRouteSettingsCommand, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
50
50
  import { CreateStateMachineCommand, DeleteStateMachineCommand, DescribeStateMachineCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$11, SFNClient, StateMachineDoesNotExist, TagResourceCommand as TagResourceCommand$13, UntagResourceCommand as UntagResourceCommand$13, UpdateStateMachineCommand } from "@aws-sdk/client-sfn";
51
51
  import { CreateClusterCommand, CreateServiceCommand, DeleteClusterCommand, DeleteServiceCommand, DeregisterTaskDefinitionCommand, DescribeClustersCommand, DescribeServicesCommand, DescribeTaskDefinitionCommand, ECSClient, PutClusterCapacityProvidersCommand, RegisterTaskDefinitionCommand, TagResourceCommand as TagResourceCommand$14, UntagResourceCommand as UntagResourceCommand$14, UpdateClusterCommand, UpdateServiceCommand, waitUntilServicesStable } from "@aws-sdk/client-ecs";
@@ -2579,6 +2579,31 @@ function isPromptAbortError(error) {
2579
2579
  * intent** — does the user want SDK + silent drop, or CC migration?
2580
2580
  * Fail fast and let the user pick one strategy per resource.
2581
2581
  */
2582
+ const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
2583
+ /**
2584
+ * The CFn type of a nested stack's row in its PARENT's template. It decides
2585
+ * both the refusal (`blockedNestedStackTargets`) and the evidence the
2586
+ * unknown-id note renders (`nestedStackLogicalIds`) — so within this module it
2587
+ * is spelled once, or the note could name ids the validator does not refuse.
2588
+ *
2589
+ * The two are NOT the same set, and deliberately so. The refusal fires when
2590
+ * EITHER the state record or the template says nested, because the two types
2591
+ * select different halves of the destructive path: the TEMPLATE's type picks
2592
+ * the provider that deletes the old resource (a Type change emits an UPDATE
2593
+ * carrying `resourceType: desiredResource.Type`, which is what
2594
+ * `oldDeleteProvider` resolves from), while the STATE's type is what
2595
+ * `NestedStackProvider` owns and derives `<parent>~<logicalId>` from. The note
2596
+ * keys on the TEMPLATE alone, because it is telling the user what their current
2597
+ * template contains. Which type governs was argued from plausibility twice and
2598
+ * settled by reading `analyzer/diff-calculator.ts` and `deploy-engine.ts`'s
2599
+ * `oldDeleteProvider`; do not re-derive it from either name. Other modules spell it
2600
+ * for themselves rather than sharing one export: the only EXPORTED copy lives
2601
+ * in `src/cli/commands/retire-cfn-stack.ts`, and importing a CLI command module
2602
+ * from the deployment layer would invert the dependency direction. (No count of
2603
+ * the other spellings is given here on purpose — an unfenced number in a
2604
+ * comment is a number that goes stale.)
2605
+ */
2606
+ const NESTED_STACK_RESOURCE_TYPE$1 = "AWS::CloudFormation::Stack";
2582
2607
  /**
2583
2608
  * Plan-time validation of the user's recreate-via-cc-api list.
2584
2609
  *
@@ -2592,7 +2617,6 @@ function isPromptAbortError(error) {
2592
2617
  * Input order is preserved; duplicate logical ids in the user's input
2593
2618
  * are deduplicated.
2594
2619
  */
2595
- const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
2596
2620
  function validateRecreateTargets(input) {
2597
2621
  const seenCcApi = new Set(input.recreateViaCcApi);
2598
2622
  const seenSdk = new Set(input.recreateViaSdkProvider ?? []);
@@ -2605,6 +2629,7 @@ function validateRecreateTargets(input) {
2605
2629
  const ambiguousIntentSdk = [];
2606
2630
  const blockedStatefulTargets = [];
2607
2631
  const blockedMultiRegionTargets = [];
2632
+ const blockedNestedStackTargets = [];
2608
2633
  const blockedAlreadySdk = [];
2609
2634
  const blockedAlreadyCcApi = [];
2610
2635
  const blockedNoSdkProvider = [];
@@ -2635,11 +2660,12 @@ function validateRecreateTargets(input) {
2635
2660
  logicalId,
2636
2661
  resourceType,
2637
2662
  physicalId: recordedResource.physicalId,
2638
- statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties),
2663
+ statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties, recordedResource.observedProperties),
2639
2664
  direction
2640
2665
  };
2641
2666
  targets.push(target);
2642
2667
  if (MULTI_REGION_RECREATE_BLOCKED_TYPES.has(resourceType)) blockedMultiRegionTargets.push(target);
2668
+ if (resourceType === NESTED_STACK_RESOURCE_TYPE$1 || templateResource.Type === NESTED_STACK_RESOURCE_TYPE$1) blockedNestedStackTargets.push(target);
2643
2669
  if (direction === "to-cc-api") {
2644
2670
  const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, EMPTY_ALLOW_SET$1);
2645
2671
  for (const { property } of actionableDrops) {
@@ -2671,10 +2697,12 @@ function validateRecreateTargets(input) {
2671
2697
  ambiguousIntentSdk,
2672
2698
  blockedStatefulTargets,
2673
2699
  blockedMultiRegionTargets,
2700
+ blockedNestedStackTargets,
2674
2701
  blockedAlreadySdk,
2675
2702
  blockedAlreadyCcApi,
2676
2703
  blockedNoSdkProvider,
2677
- conflictingDirections
2704
+ conflictingDirections,
2705
+ nestedStackLogicalIds: Object.entries(input.template.Resources ?? {}).filter(([, resource]) => resource?.Type === NESTED_STACK_RESOURCE_TYPE$1).map(([id]) => id)
2678
2706
  };
2679
2707
  }
2680
2708
  /**
@@ -2691,6 +2719,8 @@ function renderRecreateTargetsErrors(validation) {
2691
2719
  lines.push(`${FLAG_UMBRELLA} named ${validation.unknownLogicalIds.length} logical id(s) not present in the synth template:`);
2692
2720
  for (const id of validation.unknownLogicalIds) lines.push(` - ${id}`);
2693
2721
  lines.push(" Fix: confirm each id exists in the template (CDK display path is the parent; the logical id is the CFn-emitted name, e.g. cdkd synth | jq '.Resources | keys'). Recreate operates on the synth template's logical ids, not CDK display paths.");
2722
+ lines.push(" Note: each stack of this deploy validates this WHOLE flag list against its OWN template, so an id declared by a different stack of the same run is reported here as unknown. Name it in a deploy of only that stack.");
2723
+ if (validation.nestedStackLogicalIds.length > 0) lines.push(` Note: resources inside a nested stack are NOT addressable — the flags name logical ids of the stack being deployed, and this template's nested stack(s) (${validation.nestedStackLogicalIds.join(", ")}) carry their own. A logical id that a nested child happens to share with a top-level resource recreates the TOP-LEVEL one only, and the nested stack row itself is refused as a target.`);
2694
2724
  }
2695
2725
  if (validation.missingFromState.length > 0) {
2696
2726
  if (lines.length > 0) lines.push("");
@@ -2709,6 +2739,16 @@ function renderRecreateTargetsErrors(validation) {
2709
2739
  lines.push(`${FLAG_UMBRELLA} would destroy + recreate ${validation.blockedStatefulTargets.length} stateful resource(s). Recreate loses ALL data — no automatic data migration. Re-run with --force-stateful-recreation to acknowledge the data-loss footgun.`);
2710
2740
  for (const blocked of validation.blockedStatefulTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType}) — ${renderStatefulReason(blocked.statefulReason)}`);
2711
2741
  }
2742
+ if (validation.blockedNestedStackTargets.length > 0) {
2743
+ if (lines.length > 0) lines.push("");
2744
+ lines.push(`${FLAG_UMBRELLA} refuses to operate on ${validation.blockedNestedStackTargets.length} nested-stack resource(s):`);
2745
+ for (const blocked of validation.blockedNestedStackTargets) {
2746
+ const templateType = validation.nestedStackLogicalIds.includes(blocked.logicalId) ? NESTED_STACK_RESOURCE_TYPE$1 : void 0;
2747
+ const shown = templateType && templateType !== blocked.resourceType ? `${blocked.resourceType} in state, ${templateType} in the template` : blocked.resourceType;
2748
+ lines.push(` - ${blocked.logicalId} (${shown})`);
2749
+ }
2750
+ lines.push(" Recreating one would DELETE the whole child stack — every resource it owns, with no per-resource confirmation — and re-create it through a layer that does not implement cdkd's nested-stack handling. Where cdkd holds no state for that child, the delete is a no-op instead and the old resource is silently left behind. There is no --force-stateful-recreation bypass. A resource inside a child is not addressable by these flags at all.");
2751
+ }
2712
2752
  if (validation.blockedMultiRegionTargets.length > 0) {
2713
2753
  if (lines.length > 0) lines.push("");
2714
2754
  lines.push(`--recreate-via-cc-api / --recreate-via-sdk-provider refuses to operate on ${validation.blockedMultiRegionTargets.length} multi-region resource(s) — out of scope for v1 of these flags (the destroy + recreate cycle across replica regions is more involved than the single-region path):`);
@@ -3072,7 +3112,7 @@ async function promptRecreateConfirm(input) {
3072
3112
  else if (toCcCount > 0) logger.warn(`--recreate-via-cc-api will destroy + recreate ${toCcCount} resource(s) via Cloud Control API on stack ${input.stackName}:`);
3073
3113
  else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
3074
3114
  for (const t of input.targets) {
3075
- const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0) : null;
3115
+ const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0, void 0) : null;
3076
3116
  const rederived = rederivedReason !== null;
3077
3117
  const reason = t.statefulReason ?? rederivedReason;
3078
3118
  const stateful = reason !== null;
@@ -15996,209 +16036,6 @@ function isRecord$1(value) {
15996
16036
  return typeof value === "object" && value !== null && !Array.isArray(value);
15997
16037
  }
15998
16038
 
15999
- //#endregion
16000
- //#region src/provisioning/dynamodb-warm-throughput.ts
16001
- /**
16002
- * The two DynamoDB `WarmThroughput` rules that `AWS::DynamoDB::Table` and
16003
- * `AWS::DynamoDB::GlobalTable` both need, in ONE spelling.
16004
- *
16005
- * `WarmThroughput` is the same CloudFormation block on both types, and both
16006
- * providers have to answer the same two questions about it before an
16007
- * `UpdateTable` / `CreateTable` goes out:
16008
- *
16009
- * 1. **Is this block SENDABLE, and as what numbers?** CloudFormation is
16010
- * stringly typed, so `{ReadUnitsPerSecond: '12000'}` — or anything that
16011
- * came back from an `Fn::Sub` — arrives as a STRING and must not be
16012
- * forwarded verbatim into a numeric `Long` field.
16013
- * 2. **Would sending it LOWER what AWS already reports?** Warm throughput
16014
- * only ever rises with a table's traffic and AWS REJECTS a call that
16015
- * lowers it (`decreasing WarmThroughput is not supported`, measured live
16016
- * us-east-1 2026-08-13 for issue #1768).
16017
- *
16018
- * **Provenance, stated precisely because it is easy to over-claim.** Exactly
16019
- * ONE of these rules ever shipped: `AWS::DynamoDB::Table` got them in PR #1808
16020
- * (issues #1760 / #1768), and the `AWS::DynamoDB::GlobalTable` side never
16021
- * existed outside the change that created this file (issue #1857). So this is
16022
- * the Table rule LIFTED — not two shipped rules reconciled, and no deployed
16023
- * behaviour changed for either type when it moved here.
16024
- *
16025
- * What WAS compared is the lifted rule against the GlobalTable spelling
16026
- * drafted alongside it, over the shapes both had to answer (a quoted numeric
16027
- * string, a partially usable block, a mixed decrease/increase, an absent live
16028
- * value, a zero, a negative, a non-numeric string, a whitespace string, an
16029
- * explicit `undefined` / `null`, a boolean, an empty block, a scalar, an
16030
- * array). {@link isWarmThroughputDecrease} agreed on every one; the coercion
16031
- * agreed on all but a whitespace-only string, where a bare `Number(' ')` is
16032
- * `0` rather than `NaN`. This module keeps the REFUSING answer for that shape,
16033
- * since `' '` is not a capacity anyone declared and a warm throughput of 0
16034
- * is not a request AWS can honour. That is a DRAFT reconciled against a
16035
- * shipped rule, which is worth less than two shipped rules agreeing — the
16036
- * reason to read the probe list as a design note rather than as field
16037
- * evidence.
16038
- *
16039
- * Living here rather than in either provider is the point. A decrease guard is
16040
- * three clauses that all FAIL OPEN for different reasons, and two files
16041
- * spelling it independently is two chances for a later "fix" to change one of
16042
- * them — at which point the sibling type silently keeps the old answer, and
16043
- * nothing in the tree says which is intended. Same class as
16044
- * `emr-configuration.ts`, and the same reason.
16045
- *
16046
- * Deliberately NOT here: everything only ONE provider has. The `Table` side's
16047
- * `isSendableWarmThroughput` / `isRefusedWarmThroughput` /
16048
- * `declaresWarmThroughput` / `warmThroughputAlreadyMatches` are its drift-side
16049
- * and already-matches gates, which `GlobalTable` has no counterpart to (issue
16050
- * #1742 strips the per-index `WarmThroughput` from BOTH of its drift
16051
- * comparison sides unconditionally, so drift never asks the question there);
16052
- * the `GlobalTable` side's `warmThroughputDiagnostic` builds a
16053
- * `ThroughputDiagnostic` that only that provider's collector understands.
16054
- * Moving a helper with one caller here would buy nothing and cost a hop.
16055
- *
16056
- * Issues: #1760 / #1768 (Table, PR #1808), #1857 (GlobalTable).
16057
- */
16058
- /**
16059
- * The two `WarmThroughput` members, in the ONE order every message, every
16060
- * comparison and every emitted block uses. A shared order is what makes two
16061
- * blocks carrying the same numbers compare equal by `deepEqual` and serialize
16062
- * to the same wire bytes regardless of the order the template wrote them in.
16063
- */
16064
- const WARM_THROUGHPUT_MEMBERS = ["ReadUnitsPerSecond", "WriteUnitsPerSecond"];
16065
- /**
16066
- * A CloudFormation-borne numeric property, or `undefined` when the value is
16067
- * not a usable number.
16068
- *
16069
- * Plain `Number()` coercion is NOT good enough and the tree learned it three
16070
- * separate times: `Number(null)`, `Number('')`, `Number([])`, `Number(false)`
16071
- * and `Number(' ')` are all **0**, not `NaN` — so a live `0` would compare
16072
- * EQUAL to a desired `null` / `''` / `[]` / `false`, and a whitespace-only
16073
- * string would be forwarded as a request for zero units.
16074
- *
16075
- * A YAML-borne numeric STRING is still accepted, because that is a real
16076
- * template shape and `'12000'` genuinely means 12000.
16077
- *
16078
- * EXPORTED, and not because warm throughput needs it exported. This exact rule
16079
- * was hand-written three times — here, as `dynamodb-table-provider.ts`'s
16080
- * `capacityNumber` (byte-identical), and as `dynamodb-globaltable-provider.ts`'s
16081
- * `toFiniteNumber` (a different spelling of the same total function) — which is
16082
- * precisely the divergence this module exists to stop: the next "fix" to one of
16083
- * them would have left the other two answering differently, with nothing in the
16084
- * tree saying which was intended. It reads `ReadCapacityUnits` /
16085
- * `MaxReadRequestUnits` / `MinCapacity` as readily as it reads
16086
- * `ReadUnitsPerSecond`; there is only ever one right answer for "is this
16087
- * stringly-typed CFn value a number I can send?".
16088
- */
16089
- function toFiniteNumber(value) {
16090
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
16091
- if (typeof value === "string" && value.trim() !== "") {
16092
- const n = Number(value);
16093
- return Number.isFinite(n) ? n : void 0;
16094
- }
16095
- }
16096
- /**
16097
- * Coerce a CFn `WarmThroughput` block to the numeric shape the SDK's `Long`
16098
- * fields accept, PER MEMBER.
16099
- *
16100
- * PER MEMBER, not whole-block, and that distinction is the point: a block
16101
- * whose write half is an unresolved intrinsic still has a perfectly good read
16102
- * half, and dropping both would silently discard a value the template really
16103
- * did ask for. The dropped member is NAMED (`droppedMembers`) so the caller's
16104
- * warning can say which half went missing instead of reporting the whole
16105
- * property.
16106
- *
16107
- * A block with NO usable member yields `spec: undefined` — refused rather than
16108
- * forwarded, because forwarding a malformed block surfaces as an opaque AWS
16109
- * validation error naming neither cdkd nor the property. `droppedMembers` is
16110
- * still populated in that case, so a REFUSAL message can name what it refused;
16111
- * a block that is not an object at all (an unresolved `Fn::If`, a scalar, an
16112
- * array) has no member to name and reports none, which is what lets a caller
16113
- * word "one half went missing" differently from "the block is unusable".
16114
- *
16115
- * Pure: takes the raw bag, returns numbers, logs nothing. Each caller owns its
16116
- * own message, so the wording stays consistent across that provider's send
16117
- * sites without forcing one wording across both providers.
16118
- */
16119
- function coerceWarmThroughput$1(raw) {
16120
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { droppedMembers: [] };
16121
- const bag = raw;
16122
- const spec = {};
16123
- const droppedMembers = [];
16124
- for (const member of WARM_THROUGHPUT_MEMBERS) {
16125
- if (bag[member] === void 0) continue;
16126
- const coerced = toFiniteNumber(bag[member]);
16127
- if (coerced === void 0) {
16128
- droppedMembers.push(member);
16129
- continue;
16130
- }
16131
- spec[member] = coerced;
16132
- }
16133
- if (Object.keys(spec).length === 0) return { droppedMembers };
16134
- return {
16135
- spec,
16136
- droppedMembers
16137
- };
16138
- }
16139
- /**
16140
- * Whether the COERCED desired `WarmThroughput` would LOWER what AWS already
16141
- * reports.
16142
- *
16143
- * Measured live (us-east-1, 2026-08-13, issue #1768) against a table AWS
16144
- * reports `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
16145
- *
16146
- * ```
16147
- * UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
16148
- * ValidationException: One or more parameter values were invalid: Requested
16149
- * ReadUnitsPerSecond for WarmThroughput for table is lower than current
16150
- * WarmThroughput, decreasing WarmThroughput is not supported
16151
- * UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
16152
- * UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
16153
- * UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
16154
- * ```
16155
- *
16156
- * So the value is one AWS raises with the table's traffic and never lowers,
16157
- * and a decrease is REJECTED rather than accepted-and-ignored — which is what
16158
- * had to be measured before choosing, because the two produce different
16159
- * correct answers. The caller SKIPS the call on a true here.
16160
- *
16161
- * Evaluated on the COERCED spec, never on the raw bag: analysing the raw bag
16162
- * makes the verdict describe a request that is not the one being sent (a
16163
- * dropped member is not part of the call and must not be part of the
16164
- * comparison).
16165
- *
16166
- * Semantics, all three chosen to FAIL OPEN — i.e. to let the call through and
16167
- * leave AWS as the authority — because a false positive here silently drops a
16168
- * legitimate INCREASE, which is a real capacity change the user asked for,
16169
- * while a false negative merely reproduces the pre-fix behaviour of an
16170
- * AWS-side rejection that names the property:
16171
- * - DECLARED members only. An absent member is not a request to lower
16172
- * anything, so it takes no part in the verdict.
16173
- * - MIXED is not a decrease. One member below live and the other above means
16174
- * the call carries a genuine increase; AWS decides.
16175
- * - An absent or unusable LIVE counterpart is not a decrease. Without a
16176
- * number to compare against there is no evidence of one. Only the LIVE side
16177
- * can reach that arm: the desired side is a COERCED spec, so a malformed
16178
- * template value has already been dropped by {@link coerceWarmThroughput}.
16179
- *
16180
- * A decrease therefore requires every declared member to be at-or-below live
16181
- * AND at least one to be strictly below.
16182
- *
16183
- * The skip this drives is right for EVERY `update()` caller — the deploy
16184
- * engine, `cdkd drift --revert`, and the rollback executor's two revert arms —
16185
- * because none of them can make AWS lower the value, so none loses anything a
16186
- * doomed call would have achieved.
16187
- */
16188
- function isWarmThroughputDecrease(desired, live) {
16189
- if (desired === void 0 || live === void 0) return false;
16190
- let sawDecrease = false;
16191
- for (const member of WARM_THROUGHPUT_MEMBERS) {
16192
- if (desired[member] === void 0) continue;
16193
- const wanted = toFiniteNumber(desired[member]);
16194
- const current = toFiniteNumber(live[member]);
16195
- if (wanted === void 0 || current === void 0) return false;
16196
- if (wanted > current) return false;
16197
- if (wanted < current) sawDecrease = true;
16198
- }
16199
- return sawDecrease;
16200
- }
16201
-
16202
16039
  //#endregion
16203
16040
  //#region src/provisioning/dynamodb-index-busy-delete.ts
16204
16041
  /**
@@ -19969,9 +19806,27 @@ var DynamoDBGlobalTableProvider = class {
19969
19806
  const warn = (message) => this.logger.warn(maskSecrets(message));
19970
19807
  const debug = (message) => this.logger.debug(maskSecrets(message));
19971
19808
  debug(`Updating DynamoDB GlobalTable ${logicalId}: ${maskSecrets(physicalId)}`);
19972
- if (properties["TableName"] !== void 0 && previousProperties["TableName"] !== void 0 && properties["TableName"] !== previousProperties["TableName"]) throw new ProvisioningError(`TableName is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
19973
- if (properties["KeySchema"] !== void 0 && previousProperties["KeySchema"] !== void 0 && !deepEqual$1(properties["KeySchema"], previousProperties["KeySchema"])) throw new ProvisioningError(`KeySchema is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
19974
- if (properties["LocalSecondaryIndexes"] !== void 0 && previousProperties["LocalSecondaryIndexes"] !== void 0 && !deepEqual$1(properties["LocalSecondaryIndexes"], previousProperties["LocalSecondaryIndexes"])) throw new ProvisioningError(`LocalSecondaryIndexes is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
19809
+ const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
19810
+ const replaceRemedy = async () => {
19811
+ let guardRegion = "";
19812
+ try {
19813
+ guardRegion = await this.dynamoDBClient.config.region() ?? "";
19814
+ } catch {
19815
+ guardRegion = "";
19816
+ }
19817
+ return extractLocalDeletionProtection(previousProperties, guardRegion) === true ? protectedReplacementAdvice({
19818
+ evidence: "cdkd's recorded properties for this table carry DeletionProtectionEnabled: true for the deploy region",
19819
+ replaceFlags,
19820
+ disable: {
19821
+ before: "aws dynamodb update-table --table-name",
19822
+ identifier: physicalId,
19823
+ after: "--no-deletion-protection-enabled"
19824
+ }
19825
+ }) : `replacement required (deploy with ${replaceFlags}, or destroy + redeploy)`;
19826
+ };
19827
+ if (properties["TableName"] !== void 0 && previousProperties["TableName"] !== void 0 && properties["TableName"] !== previousProperties["TableName"]) throw new ProvisioningError(`TableName is immutable on AWS::DynamoDB::GlobalTable; ${await replaceRemedy()}`, resourceType, logicalId, physicalId);
19828
+ if (properties["KeySchema"] !== void 0 && previousProperties["KeySchema"] !== void 0 && !deepEqual$1(properties["KeySchema"], previousProperties["KeySchema"])) throw new ProvisioningError(`KeySchema is immutable on AWS::DynamoDB::GlobalTable; ${await replaceRemedy()}`, resourceType, logicalId, physicalId);
19829
+ if (properties["LocalSecondaryIndexes"] !== void 0 && previousProperties["LocalSecondaryIndexes"] !== void 0 && !deepEqual$1(properties["LocalSecondaryIndexes"], previousProperties["LocalSecondaryIndexes"])) throw new ProvisioningError(`LocalSecondaryIndexes is immutable on AWS::DynamoDB::GlobalTable; ${await replaceRemedy()}`, resourceType, logicalId, physicalId);
19975
19830
  const oldAttrs = previousProperties["AttributeDefinitions"] ?? [];
19976
19831
  const newAttrs = properties["AttributeDefinitions"] ?? [];
19977
19832
  const desiredKeyAttrNames = collectDesiredKeyAttributeNames(properties);
@@ -19984,7 +19839,7 @@ var DynamoDBGlobalTableProvider = class {
19984
19839
  const describeResp = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
19985
19840
  const tableArn = describeResp.Table?.TableArn;
19986
19841
  const extractLocalTags = (props) => {
19987
- return (props["Replicas"] ?? []).find((r) => r["Region"] === currentRegion)?.["Tags"];
19842
+ return localReplicaEntry(props, currentRegion)?.["Tags"];
19988
19843
  };
19989
19844
  if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
19990
19845
  const liveWarmByIndexName = /* @__PURE__ */ new Map();
@@ -21034,7 +20889,7 @@ var DynamoDBGlobalTableProvider = class {
21034
20889
  return entry;
21035
20890
  }));
21036
20891
  result["Replicas"] = replicas;
21037
- const localReplicaEntry = replicas.find((r) => r["Region"] === currentRegion);
20892
+ const localReplica = replicas.find((r) => r["Region"] === currentRegion);
21038
20893
  if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
21039
20894
  const cfnIndexes = [];
21040
20895
  const replicaIndexEntries = [];
@@ -21065,11 +20920,11 @@ var DynamoDBGlobalTableProvider = class {
21065
20920
  if (Object.keys(replicaEntry).length > 1) replicaIndexEntries.push(replicaEntry);
21066
20921
  }
21067
20922
  result["GlobalSecondaryIndexes"] = cfnIndexes;
21068
- if (localReplicaEntry && replicaIndexEntries.length > 0) localReplicaEntry["GlobalSecondaryIndexes"] = replicaIndexEntries;
20923
+ if (localReplica && replicaIndexEntries.length > 0) localReplica["GlobalSecondaryIndexes"] = replicaIndexEntries;
21069
20924
  }
21070
20925
  if (table.OnDemandThroughput?.MaxWriteRequestUnits !== void 0) result["WriteOnDemandThroughputSettings"] = { MaxWriteRequestUnits: table.OnDemandThroughput.MaxWriteRequestUnits };
21071
20926
  else result["WriteOnDemandThroughputSettings"] = {};
21072
- if (localReplicaEntry && table.OnDemandThroughput?.MaxReadRequestUnits !== void 0) localReplicaEntry["ReadOnDemandThroughputSettings"] = { MaxReadRequestUnits: table.OnDemandThroughput.MaxReadRequestUnits };
20927
+ if (localReplica && table.OnDemandThroughput?.MaxReadRequestUnits !== void 0) localReplica["ReadOnDemandThroughputSettings"] = { MaxReadRequestUnits: table.OnDemandThroughput.MaxReadRequestUnits };
21073
20928
  if (billingMode === "PROVISIONED") {
21074
20929
  const autoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:table:WriteCapacityUnits");
21075
20930
  if (autoScaling) result["WriteProvisionedThroughputSettings"] = { WriteCapacityAutoScalingSettings: autoScaling };
@@ -21638,10 +21493,29 @@ var DynamoDBGlobalTableProvider = class {
21638
21493
  }
21639
21494
  };
21640
21495
  /**
21641
- * Diff CFn `Replicas[]` arrays. Keyed by `Region`. Returns adds, removes,
21642
- * and modifies (entries whose other keys — KMSMasterKeyId,
21643
- * GlobalSecondaryIndexes, TableClassOverride differ from the old shape).
21496
+ * The `Replicas[]` entry for `region`, or `undefined`.
21497
+ *
21498
+ * ONE predicate, because two readers of the same field had drifted: issue
21499
+ * [#2610]'s review round guarded `extractLocalDeletionProtection` against a
21500
+ * non-array `Replicas` and left `update()`'s inline `extractLocalTags` on the
21501
+ * old `(props['Replicas'] ?? []) as Array<...>` -- which is the one that
21502
+ * actually throws FIRST on the apply path, ~400 lines earlier in the same
21503
+ * method, so guarding only the other produced no visible change and no test
21504
+ * went red. A shared predicate is what stops the next reader inheriting the
21505
+ * unguarded spelling; `derivePerCallProvisionedThroughput` documents the same
21506
+ * hazard for the same field and guards independently.
21507
+ *
21508
+ * `Array.isArray`, not `?? []`: a non-array `Replicas` -- an unresolved
21509
+ * intrinsic, or a hand-edited state record -- makes `.find` throw a bare
21510
+ * `TypeError`, and since [#2610] one caller sits on the REFUSAL path, where a
21511
+ * throw REPLACES three informative `ProvisioningError`s with a stack trace.
21512
+ * The `r?.` guards a null ENTRY inside an otherwise-valid array.
21644
21513
  */
21514
+ function localReplicaEntry(props, region) {
21515
+ const raw = props["Replicas"];
21516
+ if (!Array.isArray(raw)) return void 0;
21517
+ return raw.find((r) => r?.["Region"] === region);
21518
+ }
21645
21519
  /**
21646
21520
  * Extract the local replica's `DeletionProtectionEnabled` from a CFn
21647
21521
  * properties shape. The `AWS::DynamoDB::GlobalTable` CFn schema places
@@ -21657,7 +21531,7 @@ var DynamoDBGlobalTableProvider = class {
21657
21531
  * the SDK call.
21658
21532
  */
21659
21533
  function extractLocalDeletionProtection(props, region) {
21660
- const perReplica = (props["Replicas"] ?? []).find((r) => r["Region"] === region)?.["DeletionProtectionEnabled"];
21534
+ const perReplica = localReplicaEntry(props, region)?.["DeletionProtectionEnabled"];
21661
21535
  if (typeof perReplica === "boolean") return perReplica;
21662
21536
  const topLevel = props["DeletionProtectionEnabled"];
21663
21537
  return typeof topLevel === "boolean" ? topLevel : void 0;
@@ -22916,6 +22790,11 @@ function collectTableOnDemandCeilings(properties, region, diagnostics, maskSecre
22916
22790
  }
22917
22791
  };
22918
22792
  }
22793
+ /**
22794
+ * Diff CFn `Replicas[]` arrays. Keyed by `Region`. Returns adds, removes,
22795
+ * and modifies (entries whose other keys — KMSMasterKeyId,
22796
+ * GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
22797
+ */
22919
22798
  function diffReplicas(oldReplicas, newReplicas) {
22920
22799
  const oldByRegion = /* @__PURE__ */ new Map();
22921
22800
  const newByRegion = /* @__PURE__ */ new Map();
@@ -22984,6 +22863,75 @@ function deepEqual$1(a, b) {
22984
22863
  //#endregion
22985
22864
  //#region src/provisioning/providers/logs-loggroup-provider.ts
22986
22865
  /**
22866
+ * Refuse a `RetentionInDays` the send path cannot USE (issue [#2521]).
22867
+ *
22868
+ * **The scope of this refusal is a TEST, not a paragraph.** Every review round
22869
+ * on issue [#2521] found a hole in a PROSE statement of it — a coercible
22870
+ * negative that fell through to `DeleteRetentionPolicy`, an unusable value
22871
+ * silenced by the wrong change gate, wrong cells in the hand-written table
22872
+ * that replaced those sentences, and then the one non-finite number
22873
+ * JavaScript makes FALSY. No count is given here: the count itself drifted
22874
+ * between this comment and the changelog, which is the same failure one level
22875
+ * up. The table now lives in
22876
+ * `tests/unit/provisioning/logs-loggroup-provider-retention-coercion.test.ts`
22877
+ * as `CREATE_MATRIX` / `UPDATE_MATRIX`: one row per (value, previous) pair,
22878
+ * each asserting which AWS call goes out, each carrying what the pre-#2521
22879
+ * code did. Read a change against those rows and add one for any pair they do
22880
+ * not cover; do not restate the rule here, because a fourth restatement is
22881
+ * the thing that keeps being wrong.
22882
+ *
22883
+ * THREE refusal CASES across TWO `throw` statements — the non-finite guard
22884
+ * clause at the top of the body has its own, and the other two share the
22885
+ * second, differing only in the `detail` clause. Only the WHY of each:
22886
+ *
22887
+ * - **a non-finite NUMBER** (`NaN`, `Infinity`, `-Infinity`). It sits ahead
22888
+ * of the falsy return because `NaN` is FALSY: without it that value exits
22889
+ * early, fails the `> 0` send test and reaches `DeleteRetentionPolicy`,
22890
+ * while its two siblings — truthy — were already refused. Leaving one
22891
+ * member of a family out is the inconsistency the negative arm below
22892
+ * introduced, so the arm names the family.
22893
+ * - **not a number at all** (`' '`, `'abc'`, `[]`, `{}`, `true`). The
22894
+ * pre-coercion code tested `if (properties['RetentionInDays'])` and handed
22895
+ * the raw value to `PutRetentionPolicyCommand`, so CloudWatch Logs rejected
22896
+ * it. Refusing moves that rejection one layer earlier and names the
22897
+ * property.
22898
+ * - **a NEGATIVE number** (`-1`, `'-1'`). It coerces, so a refusal gated on
22899
+ * coercibility alone let it through; it then failed the `> 0` send test and
22900
+ * fell into the UPDATE path's `else`, which issues
22901
+ * `DeleteRetentionPolicy` — a template typo SILENTLY REMOVING a live
22902
+ * retention where the old code sent `-1` and failed loudly.
22903
+ *
22904
+ * The FALSY values pass through, as they did before — `0`, `''`, `false`,
22905
+ * `null` and absent; `NaN` is the one exception, refused above because it is a
22906
+ * malformed number rather than a spelling of "no retention".
22907
+ *
22908
+ * `'0'` is TRUTHY, so it reaches this function and passes too: zero is
22909
+ * what {@link LogsLogGroupProvider.readCurrentState} records for a group with
22910
+ * no retention policy, and CloudFormation coerces `'0'` to `0`, so routing the
22911
+ * string to the same arm as the number is the consistency the coercion is for.
22912
+ * On the UPDATE path against a live positive retention that is a real change —
22913
+ * it DELETES where the old code sent and was rejected — and `UPDATE_MATRIX`
22914
+ * pins it.
22915
+ *
22916
+ * The message names the value's TYPE and never the value. A provider's
22917
+ * `properties` bag arrives with dynamic references already RESOLVED, so a
22918
+ * `{{resolve:secretsmanager:...}}` scalar is PLAINTEXT by the time it reaches
22919
+ * here, and this function has no masker to thread — `create()` takes no
22920
+ * `CreateContext` and `applyUpdate` no `UpdateContext`
22921
+ * (`.claude/rules/provider-masking.md`). Interpolating it as
22922
+ * `${JSON.stringify(raw)}` was the first draft and
22923
+ * `vp run audit:provider-secret-mask:check` refused it — but read that as one
22924
+ * SPELLING being fenced, not the rule: the critic's population is
22925
+ * interpolated `JSON.stringify` calls, so a bare `${raw}` here would leave it
22926
+ * green. Do not put the value back without threading the capability first.
22927
+ */
22928
+ function assertUsableRetention(raw, coerced, logicalId, resourceType) {
22929
+ if (typeof raw === "number" && !Number.isFinite(raw)) throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a non-negative number or a numeric string (CloudFormation coerces '30' to 30); the template declares a non-finite number`, resourceType, logicalId);
22930
+ if (!raw) return;
22931
+ if (coerced !== void 0 && coerced >= 0) return;
22932
+ throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a non-negative number or a numeric string (CloudFormation coerces '30' to 30); ${coerced === void 0 ? `the template's value is of type ${Array.isArray(raw) ? "array" : typeof raw} and does not parse as a finite number` : "the template declares a negative number"}`, resourceType, logicalId);
22933
+ }
22934
+ /**
22987
22935
  * AWS CloudWatch Logs LogGroup Provider
22988
22936
  *
22989
22937
  * Implements resource provisioning for AWS::Logs::LogGroup using the CloudWatch Logs SDK.
@@ -23064,8 +23012,10 @@ var LogsLogGroupProvider = class {
23064
23012
  else throw createError;
23065
23013
  }
23066
23014
  try {
23067
- const retentionInDays = properties["RetentionInDays"];
23068
- if (retentionInDays) await this.logsClient.send(new PutRetentionPolicyCommand({
23015
+ const rawRetention = properties["RetentionInDays"];
23016
+ const retentionInDays = toFiniteNumber(rawRetention);
23017
+ assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
23018
+ if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
23069
23019
  logGroupName,
23070
23020
  retentionInDays
23071
23021
  }));
@@ -23135,14 +23085,14 @@ var LogsLogGroupProvider = class {
23135
23085
  */
23136
23086
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
23137
23087
  try {
23138
- return await this.applyUpdate(logicalId, physicalId, properties, previousProperties);
23088
+ return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties);
23139
23089
  } catch (error) {
23140
23090
  if (error instanceof CdkdError) throw error;
23141
23091
  const cause = error instanceof Error ? error : void 0;
23142
23092
  throw new ProvisioningError(`Failed to update log group ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
23143
23093
  }
23144
23094
  }
23145
- async applyUpdate(logicalId, physicalId, properties, previousProperties) {
23095
+ async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties) {
23146
23096
  this.logger.debug(`Updating log group ${logicalId}: ${physicalId}`);
23147
23097
  const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
23148
23098
  const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
@@ -23151,6 +23101,13 @@ var LogsLogGroupProvider = class {
23151
23101
  const replaceFlags = "--replace --force-stateful-recreation";
23152
23102
  throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. ${isTruthyCfnBoolean(previousProperties["DeletionProtectionEnabled"]) ? `cdkd's recorded properties for this log group carry DeletionProtectionEnabled, so ${replaceFlags} alone will NOT succeed while AWS still has it on: the replacement normally deletes the log group, AWS refuses that delete while protection is on, and cdkd deploy has no --remove-protection flag to clear it (only cdkd destroy and cdkd state destroy act on one). Read "Deletion protection blocks a replacement" in docs/cli-deploy-safety.md BEFORE you disable anything: whether disabling helps at all, and what the flag ends up as, depend on your UpdateReplacePolicy and on whether the deploy completes — neither of which this refusal can see. Then disable deletion protection — \`aws logs put-log-group-deletion-protection --log-group-identifier '${physicalId}' --no-deletion-protection-enabled\`, or via the console — and re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost). Setting DeletionProtectionEnabled: false in the template does NOT clear it in the same deploy: this refusal fires before that property is applied, so that route needs its own deploy with the LogGroupClass change reverted. Or revert the LogGroupClass change and keep the current class.` : `Re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change.`} Note --force-stateful-recreation has NO per-resource granularity: it clears the data guard for every replacement target in the run`);
23153
23103
  }
23104
+ const rawRetention = properties["RetentionInDays"];
23105
+ const rawPreviousRetention = previousProperties["RetentionInDays"];
23106
+ const renderedRetention = JSON.stringify(rawRetention);
23107
+ const sameRawRetention = Object.is(rawRetention, rawPreviousRetention) || renderedRetention !== void 0 && renderedRetention !== "null" && renderedRetention === JSON.stringify(rawPreviousRetention);
23108
+ const retentionInDays = toFiniteNumber(rawRetention);
23109
+ const oldRetentionInDays = toFiniteNumber(rawPreviousRetention);
23110
+ if (!sameRawRetention) assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
23154
23111
  const newKmsKeyId = properties["KmsKeyId"];
23155
23112
  if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
23156
23113
  if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
@@ -23160,8 +23117,7 @@ var LogsLogGroupProvider = class {
23160
23117
  else await this.logsClient.send(new DisassociateKmsKeyCommand({ logGroupName: physicalId }));
23161
23118
  this.logger.debug(`Updated KMS key association for log group ${physicalId}`);
23162
23119
  }
23163
- const retentionInDays = properties["RetentionInDays"];
23164
- if (retentionInDays !== previousProperties["RetentionInDays"]) if (retentionInDays) await this.logsClient.send(new PutRetentionPolicyCommand({
23120
+ if (retentionInDays !== oldRetentionInDays || !sameRawRetention && Boolean(rawPreviousRetention) && oldRetentionInDays === void 0) if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
23165
23121
  logGroupName: physicalId,
23166
23122
  retentionInDays
23167
23123
  }));
@@ -37258,7 +37214,15 @@ var ELBv2Provider = class {
37258
37214
  for (const [k, v] of Object.entries(p)) if (!handledKeys.has(k)) out[k] = v;
37259
37215
  return out;
37260
37216
  };
37261
- if (JSON.stringify(stripHandled(properties)) !== JSON.stringify(stripHandled(previousProperties))) throw new ResourceUpdateNotSupportedError("AWS::ElasticLoadBalancingV2::LoadBalancer", logicalId, "ELBv2 LoadBalancer Name / Type / Scheme are immutable on AWS — none of the ELBv2 Modify* / Set* APIs accept these fields; they are fixed at creation. cdkd handles LoadBalancerAttributes / Subnets / SubnetMappings / SecurityGroups / IpAddressType / Tags in-place; for Name / Type / Scheme re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.");
37217
+ if (JSON.stringify(stripHandled(properties)) !== JSON.stringify(stripHandled(previousProperties))) throw new ResourceUpdateNotSupportedError("AWS::ElasticLoadBalancingV2::LoadBalancer", logicalId, "ELBv2 LoadBalancer Name / Type / Scheme are immutable on AWS — none of the ELBv2 Modify* / Set* APIs accept these fields; they are fixed at creation. cdkd handles LoadBalancerAttributes / Subnets / SubnetMappings / SecurityGroups / IpAddressType / Tags in-place. " + (this.normalizeAttributes(previousProperties["LoadBalancerAttributes"]).some((a) => a.Key === "deletion_protection.enabled" && isTruthyCfnBoolean(a.Value)) ? protectedReplacementAdvice({
37218
+ evidence: "cdkd's recorded properties for this load balancer carry LoadBalancerAttributes deletion_protection.enabled=true",
37219
+ replaceFlags: "cdkd deploy --replace",
37220
+ disable: {
37221
+ before: "aws elbv2 modify-load-balancer-attributes --load-balancer-arn",
37222
+ identifier: physicalId,
37223
+ after: "--attributes Key=deletion_protection.enabled,Value=false"
37224
+ }
37225
+ }) : "For Name / Type / Scheme re-deploy with cdkd deploy --replace, or destroy + redeploy the stack."));
37262
37226
  const submittedAttrs = this.diffAttributes(this.normalizeAttributes(properties["LoadBalancerAttributes"]), this.normalizeAttributes(previousProperties["LoadBalancerAttributes"]), this.attributeRemovalResolver(LOAD_BALANCER_ATTRIBUTE_DEFAULTS));
37263
37227
  if (submittedAttrs.length > 0) {
37264
37228
  await this.getClient().send(new ModifyLoadBalancerAttributesCommand({
@@ -43897,7 +43861,7 @@ var CognitoUserPoolProvider = class {
43897
43861
  if (properties["SmsVerificationMessage"] !== void 0) updateParams.SmsVerificationMessage = properties["SmsVerificationMessage"];
43898
43862
  if (properties["UserPoolTier"]) updateParams.UserPoolTier = properties["UserPoolTier"];
43899
43863
  await this.getClient().send(new UpdateUserPoolCommand(updateParams));
43900
- await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"]);
43864
+ await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"], (properties["DeletionProtection"] ? properties["DeletionProtection"] : previousProperties["DeletionProtection"]) === "ACTIVE");
43901
43865
  await this.applyMfaConfig(physicalId, properties, mfaConfiguration, {
43902
43866
  ...liveMfa?.value !== void 0 ? { liveMfaConfiguration: liveMfa.value } : {},
43903
43867
  ...liveMfa?.failed === true ? { liveReadFailed: true } : {},
@@ -43939,7 +43903,7 @@ var CognitoUserPoolProvider = class {
43939
43903
  * attribute, requires replacement -> ResourceUpdateNotSupportedError.
43940
43904
  * A byte-identical Schema is a no-op (no AddCustomAttributes call).
43941
43905
  */
43942
- async reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, newSchema, oldSchema) {
43906
+ async reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, newSchema, oldSchema, deletionProtected = false) {
43943
43907
  const newAttrs = newSchema ?? [];
43944
43908
  const oldAttrs = oldSchema ?? [];
43945
43909
  for (const attr of newAttrs) if (attr.Name === void 0) throw new ProvisioningError(`Cognito User Pool ${logicalId} has a Schema attribute with no Name — every Schema entry must have a Name`, resourceType, logicalId, physicalId);
@@ -43964,7 +43928,20 @@ var CognitoUserPoolProvider = class {
43964
43928
  ...modified.map((n) => `modified attribute '${n}'`),
43965
43929
  ...addedStandard.map((n) => `added standard attribute '${n}'`)
43966
43930
  ];
43967
- if (immutableChanges.length > 0) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `the Schema change (${immutableChanges.join("; ")}) is immutable on AWS — AWS can only ADD custom attributes in place, so removing or modifying an attribute requires recreating the pool. AWS::Cognito::UserPool is a stateful type, so re-run with cdkd deploy --replace --force-stateful-recreation to recreate it (this deletes all users in the pool)`);
43931
+ if (immutableChanges.length > 0) {
43932
+ const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
43933
+ const remedy = deletionProtected ? protectedReplacementAdvice({
43934
+ evidence: "cdkd's properties for this user pool carry DeletionProtection: ACTIVE",
43935
+ replaceFlags,
43936
+ disable: {
43937
+ before: "aws cognito-idp update-user-pool --user-pool-id",
43938
+ identifier: physicalId,
43939
+ after: "--deletion-protection INACTIVE",
43940
+ caveat: "Note UpdateUserPool's behaviour on an omitted member differs per field, and is unmeasured for DeletionProtection, so send your complete pool configuration alongside that flag rather than the flag alone."
43941
+ }
43942
+ }) : `AWS::Cognito::UserPool is a stateful type, so re-run with ${replaceFlags} to recreate it (this deletes all users in the pool).`;
43943
+ throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `the Schema change (${immutableChanges.join("; ")}) is immutable on AWS — AWS can only ADD custom attributes in place, so removing or modifying an attribute requires recreating the pool. ${remedy}`);
43944
+ }
43968
43945
  const addedCustom = added.filter((a) => a.Name !== void 0 && !STANDARD_USER_POOL_ATTRIBUTES.has(a.Name));
43969
43946
  if (addedCustom.length === 0) return;
43970
43947
  this.logger.debug(`Adding ${addedCustom.length} custom attribute(s) to ${physicalId}: ` + addedCustom.map((a) => a.Name).join(", "));
@@ -52048,7 +52025,7 @@ var SchedulerScheduleProvider = class {
52048
52025
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
52049
52026
  const groupName = this.groupNameOf(properties);
52050
52027
  const previousGroupName = this.groupNameOf(previousProperties);
52051
- if (groupName !== previousGroupName) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `GroupName addresses the schedule (${previousGroupName ?? "default"} -> ${groupName ?? "default"}); re-run with \`cdkd deploy --replace ${logicalId}\` to recreate it in the new group`);
52028
+ if (groupName !== previousGroupName) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `GroupName addresses the schedule (${previousGroupName ?? "default"} -> ${groupName ?? "default"}); re-run with \`cdkd deploy --replace\` to recreate it in the new group (--replace is a boolean flag and takes no resource id; it applies to every resource in the run whose in-place update is refused)`);
52052
52029
  this.logger.debug(`Updating Schedule ${logicalId}: ${physicalId}${groupName ? ` (group: ${groupName})` : ""}`);
52053
52030
  try {
52054
52031
  const input = {
@@ -52070,7 +52047,11 @@ var SchedulerScheduleProvider = class {
52070
52047
  }
52071
52048
  async delete(logicalId, physicalId, resourceType, properties, context) {
52072
52049
  const groupName = this.groupNameOf(properties);
52073
- if (properties === void 0) this.logger.warn(`State record for Schedule ${logicalId} carries no properties — deleting '${physicalId}' from the default group. If the schedule lives in a custom group, delete it manually: aws scheduler delete-schedule --name ${physicalId} --group-name <group>`);
52050
+ if (properties === void 0) {
52051
+ const safeId = displaySafe(physicalId, { asciiOnly: true });
52052
+ const manualHint = safeId && safeId === physicalId ? `If the schedule lives in a custom group, delete it manually: aws scheduler delete-schedule --name ${shellQuote(safeId)} --group-name <group>` : "If the schedule lives in a custom group, delete it manually via the console: the name recorded for it cannot be reproduced safely on a command line.";
52053
+ this.logger.warn(`State record for Schedule ${logicalId} carries no properties — deleting '${safeId || "<unrenderable>"}' from the default group. ${manualHint}`);
52054
+ }
52074
52055
  this.logger.debug(`Deleting Schedule ${logicalId}: ${physicalId}${groupName ? ` (group: ${groupName})` : ""}`);
52075
52056
  try {
52076
52057
  await this.getClient().send(new DeleteScheduleCommand({
@@ -52254,7 +52235,7 @@ var EFSProvider = class {
52254
52235
  ]) {
52255
52236
  const next = properties[key];
52256
52237
  const prev = previousProperties[key];
52257
- if (next !== void 0 && prev !== void 0 && JSON.stringify(next) !== JSON.stringify(prev)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EFS FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
52238
+ if (next !== void 0 && prev !== void 0 && JSON.stringify(next) !== JSON.stringify(prev)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EFS FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace --force-stateful-recreation (the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED), or destroy + redeploy the stack.`);
52258
52239
  }
52259
52240
  const newThroughputMode = properties["ThroughputMode"];
52260
52241
  const newProvisioned = properties["ProvisionedThroughputInMibps"];
@@ -53375,7 +53356,7 @@ var FSxFileSystemProvider = class {
53375
53356
  for (const key of TOP_LEVEL_IMMUTABLE_PROPS) {
53376
53357
  const next = properties[key];
53377
53358
  const prev = previousProperties[key];
53378
- if (JSON.stringify(next ?? null) !== JSON.stringify(prev ?? null)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
53359
+ if (JSON.stringify(next ?? null) !== JSON.stringify(prev ?? null)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace --force-stateful-recreation (the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED), or destroy + redeploy the stack.`);
53379
53360
  }
53380
53361
  const variantConfigKey = this.detectVariantConfigKey(properties, previousProperties);
53381
53362
  const { diff: variantDiff, hasMutableDiff: variantHasMutableDiff } = variantConfigKey ? this.computeVariantConfigDiff(resourceType, logicalId, variantConfigKey, properties[variantConfigKey] ?? {}, previousProperties[variantConfigKey] ?? {}) : {
@@ -53456,7 +53437,7 @@ var FSxFileSystemProvider = class {
53456
53437
  const nextVal = next[key];
53457
53438
  const prevVal = prev[key];
53458
53439
  if (JSON.stringify(nextVal) === JSON.stringify(prevVal)) continue;
53459
- if (!mutable.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${configKey}.${key} is immutable on AWS — UpdateFileSystem cannot change it after creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
53440
+ if (!mutable.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${configKey}.${key} is immutable on AWS — UpdateFileSystem cannot change it after creation. Re-deploy with cdkd deploy --replace --force-stateful-recreation (the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED), or destroy + redeploy the stack.`);
53460
53441
  const ctx = {
53461
53442
  resourceType,
53462
53443
  logicalId,
@@ -54417,6 +54398,16 @@ var EMRClusterProvider = class {
54417
54398
  const changed = (key) => !jsonEqual$2(properties[key], previousProperties[key]);
54418
54399
  const nextInstances = properties["Instances"] ?? {};
54419
54400
  const prevInstances = previousProperties["Instances"] ?? {};
54401
+ const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
54402
+ const replaceRemedy = () => toBoolean(prevInstances["TerminationProtected"]) === true ? protectedReplacementAdvice({
54403
+ evidence: "cdkd's recorded properties for this cluster carry Instances.TerminationProtected: true",
54404
+ replaceFlags,
54405
+ disable: {
54406
+ before: "aws emr modify-cluster-attributes --cluster-id",
54407
+ identifier: physicalId,
54408
+ after: "--no-termination-protected"
54409
+ }
54410
+ }) : `Re-deploy with ${replaceFlags}, or destroy + redeploy the stack.`;
54420
54411
  let terminationProtectedChanged = false;
54421
54412
  if (changed("Instances")) {
54422
54413
  const instanceKeys = /* @__PURE__ */ new Set([...Object.keys(nextInstances), ...Object.keys(prevInstances)]);
@@ -54426,7 +54417,7 @@ var EMRClusterProvider = class {
54426
54417
  terminationProtectedChanged = true;
54427
54418
  continue;
54428
54419
  }
54429
- throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster Instances.${key} is immutable on AWS — a running cluster's instance topology / networking cannot be changed in place. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
54420
+ throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster Instances.${key} is immutable on AWS — a running cluster's instance topology / networking cannot be changed in place. ${replaceRemedy()}`);
54430
54421
  }
54431
54422
  }
54432
54423
  for (const key of Object.keys({
@@ -54435,7 +54426,7 @@ var EMRClusterProvider = class {
54435
54426
  })) {
54436
54427
  if (key === "Instances") continue;
54437
54428
  if (!changed(key)) continue;
54438
- if (!MUTABLE_TOP_LEVEL_PROPS.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster ${key} is immutable on AWS — it is fixed at cluster creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
54429
+ if (!MUTABLE_TOP_LEVEL_PROPS.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster ${key} is immutable on AWS — it is fixed at cluster creation. ${replaceRemedy()}`);
54439
54430
  }
54440
54431
  const visibleChanged = changed("VisibleToAllUsers");
54441
54432
  const stepConcurrencyChanged = changed("StepConcurrencyLevel");
@@ -58447,7 +58438,7 @@ var DLMLifecyclePolicyProvider = class {
58447
58438
  }
58448
58439
  }
58449
58440
  async update(logicalId, physicalId, resourceType, properties, previousProperties) {
58450
- if (JSON.stringify(properties["DefaultPolicy"]) !== JSON.stringify(previousProperties["DefaultPolicy"])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `DefaultPolicy cannot be changed in place; re-run with \`cdkd deploy --replace ${logicalId}\` to recreate the policy`);
58441
+ if (JSON.stringify(properties["DefaultPolicy"]) !== JSON.stringify(previousProperties["DefaultPolicy"])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "DefaultPolicy cannot be changed in place; re-run with `cdkd deploy --replace` to recreate the policy (--replace is a boolean flag and takes no resource id; it applies to every resource in the run whose in-place update is refused)");
58451
58442
  this.logger.debug(`Updating DLM Lifecycle Policy ${logicalId} (${physicalId})`);
58452
58443
  try {
58453
58444
  await this.getClient().send(new UpdateLifecyclePolicyCommand({
@@ -58647,7 +58638,7 @@ var S3VectorsProvider = class {
58647
58638
  * is surfaced as `ResourceUpdateNotSupportedError`.
58648
58639
  */
58649
58640
  async updateVectorBucket(logicalId, physicalId, resourceType, properties, previousProperties) {
58650
- for (const createOnly of ["VectorBucketName", "EncryptionConfiguration"]) if (JSON.stringify(properties[createOnly]) !== JSON.stringify(previousProperties[createOnly])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `'${createOnly}' is immutable (create-only) on VectorBucket '${physicalId}'; a change requires replacement (cdkd deploy --replace)`);
58641
+ for (const createOnly of ["VectorBucketName", "EncryptionConfiguration"]) if (JSON.stringify(properties[createOnly]) !== JSON.stringify(previousProperties[createOnly])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `'${createOnly}' is immutable (create-only) on VectorBucket '${physicalId}'; a change requires replacement (cdkd deploy --replace --force-stateful-recreation — the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED)`);
58651
58642
  const oldTags = this.cfnTagsToRecord(previousProperties["Tags"]);
58652
58643
  const newTags = this.cfnTagsToRecord(properties["Tags"]);
58653
58644
  const toSet = {};
@@ -62686,8 +62677,7 @@ async function deployCommand(stacks, options) {
62686
62677
  skipPrefix,
62687
62678
  yes: options.yes
62688
62679
  });
62689
- let recreateViaCcApiTargets;
62690
- let recreateViaSdkProviderTargets;
62680
+ let recreateTargets;
62691
62681
  if (options.recreateViaCcApi?.length || options.recreateViaSdkProvider?.length) {
62692
62682
  const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
62693
62683
  const validation = await probeAndRevalidateStateful({
@@ -62716,9 +62706,12 @@ async function deployCommand(stacks, options) {
62716
62706
  });
62717
62707
  const errorBlock = renderRecreateTargetsErrors(validation);
62718
62708
  if (errorBlock) throw new CdkdError(errorBlock, "RECREATE_TARGETS_INVALID");
62719
- recreateViaCcApiTargets = new Set(validation.targets.filter((t) => t.direction === "to-cc-api").map((t) => t.logicalId));
62720
- recreateViaSdkProviderTargets = new Set(validation.targets.filter((t) => t.direction === "to-sdk").map((t) => t.logicalId));
62721
- if (recreateViaCcApiTargets.size > 0 || recreateViaSdkProviderTargets.size > 0) {
62709
+ recreateTargets = {
62710
+ stackName: stackInfo.stackName,
62711
+ viaCcApi: new Set(validation.targets.filter((t) => t.direction === "to-cc-api").map((t) => t.logicalId)),
62712
+ viaSdkProvider: new Set(validation.targets.filter((t) => t.direction === "to-sdk").map((t) => t.logicalId))
62713
+ };
62714
+ if (recreateTargets.viaCcApi.size > 0 || recreateTargets.viaSdkProvider.size > 0) {
62722
62715
  const downstreamConsumers = await findDownstreamConsumers({
62723
62716
  producerStack: stackInfo.stackName,
62724
62717
  producerRegion: stackRegion,
@@ -62743,8 +62736,7 @@ async function deployCommand(stacks, options) {
62743
62736
  ...assetRedirect && { assetRedirect },
62744
62737
  ...eventRecorder && { eventRecorder },
62745
62738
  ...migrationGate && { onCurrentStateLoaded: migrationGate },
62746
- ...recreateViaCcApiTargets && recreateViaCcApiTargets.size > 0 && { recreateViaCcApiTargets },
62747
- ...recreateViaSdkProviderTargets && recreateViaSdkProviderTargets.size > 0 && { recreateViaSdkProviderTargets },
62739
+ ...recreateTargets && (recreateTargets.viaCcApi.size > 0 || recreateTargets.viaSdkProvider.size > 0) && { recreateTargets },
62748
62740
  ...options.replace && { replace: true },
62749
62741
  ...options.forceStatefulRecreation && { forceStatefulRecreation: true },
62750
62742
  ...options.skipFinalSnapshot && { skipFinalSnapshot: true },
@@ -65301,7 +65293,7 @@ async function scrubCommand(stacks, options) {
65301
65293
  if (options.fail) throw new ScrubNeededError();
65302
65294
  return;
65303
65295
  }
65304
- if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The plaintext is no longer stored there, but a value that was ever persisted should be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating).${keyNote}${unverifiableNote}${failureNote}`);
65296
+ if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The CURRENT state.json no longer holds the plaintext. Where the state bucket is VERSIONED — which cdkd bootstrap enables — the pre-scrub body survives as a noncurrent version, stays readable with GetObject and a VersionId, and scrub does not purge it. So a value that was ever persisted must be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating); rotation is what makes any surviving version harmless.${keyNote}${unverifiableNote}${failureNote}`);
65305
65297
  else logger.info(`\nNothing could be rewritten.${keyNote}${unverifiableNote}${failureNote} ROTATE any exposed secret.`);
65306
65298
  if (failures.length > 0) throw scrubStacksFailedError(failures);
65307
65299
  if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
@@ -67962,9 +67954,10 @@ var DriftDetectedError = class DriftDetectedError extends CdkdError {
67962
67954
  * `notCompared`. The roll-up also contains resources whose only problem is a
67963
67955
  * surviving `{{resolve:...}}` token cdkd resolves for nobody — a look-alike
67964
67956
  * spelling, since issue #2482 moved `ssm-secure` onto the `ssm` path — which
67965
- * can never clear on a re-run, and which this change did not create. Exiting non-zero for them would break `cdkd drift` in CI forever over
67966
- * an unrelated defect, so the exit reads `refused` and `readFailed` while the
67967
- * report covers all three. See `NotComparedCause` and `outcomeExitSignal`.
67957
+ * can never clear on a re-run, and which this change did not create. Exiting
67958
+ * non-zero for them would break `cdkd drift` in CI forever over an unrelated
67959
+ * defect, so the exit reads `refused` and `readFailed` while the report covers
67960
+ * all three. See `NotComparedCause` and `outcomeExitSignal`.
67968
67961
  *
67969
67962
  * WHY A NON-ZERO EXIT, and why it is a PRESERVATION rather than a new signal.
67970
67963
  * Pre-#2108 that population resolved the reference in the WRONG region, the
@@ -68075,7 +68068,8 @@ function notComparedOutcomes(report) {
68075
68068
  * code covers only the CLEARABLE ones — `refused` (the population #2108 created)
68076
68069
  * and `readFailed` (issues #2151 / #1945). Exiting non-zero on
68077
68070
  * `unresolvedToken` would break `cdkd drift` in CI forever for every stack
68078
- * holding such a spelling — permanent, unclearable, and unrelated. See {@link NotComparedCause}.
68071
+ * holding such a spelling — permanent, unclearable, and unrelated. See
68072
+ * {@link NotComparedCause}.
68079
68073
  *
68080
68074
  * A `drifted` outcome reports `drifted` even when its own comparison was
68081
68075
  * refused: drift is the stronger, actionable signal and the caller ranks it
@@ -68211,10 +68205,10 @@ function uncomparedTally(reports) {
68211
68205
  *
68212
68206
  * - The TRIGGER is `anyIncomplete`, i.e. {@link outcomeExitSignal}'s
68213
68207
  * `incomplete`. It is narrow on purpose: a stack whose ONLY uncompared
68214
- * resource holds a `{{resolve:...}}` token cdkd resolves for nobody must not start
68215
- * shouting on every run about a comparison no action of the user's can ever
68216
- * complete -- the same CI-forever hazard that cause is kept out of the exit
68217
- * code on.
68208
+ * resource holds a `{{resolve:...}}` token cdkd resolves for nobody must
68209
+ * not start shouting on every run about a comparison no action of the
68210
+ * user's can ever complete -- the same CI-forever hazard that cause is
68211
+ * kept out of the exit code on.
68218
68212
  * - The COUNT and the LABELS, once the line is triggered, cover EVERY
68219
68213
  * uncompared resource, each named by its own reason (see
68220
68214
  * {@link uncomparedTally}). A resource that was not compared was not
@@ -70490,9 +70484,19 @@ function orderConsumersBeforeProducers(stackNames, consumerToProducers) {
70490
70484
  *
70491
70485
  * Purges the stack's deployment-event history (the post-mortem `deployments/`
70492
70486
  * store cdkd keeps by default) ONLY after a clean, non-interrupted destroy, so
70493
- * the state bucket returns fully empty. Deliberately skipped on a failed /
70494
- * interrupted destroy — those events ARE the post-mortem the user wants on the
70495
- * retry — and a no-op when `--purge-events` was not passed.
70487
+ * a plain object listing of the state bucket comes back empty. Deliberately
70488
+ * skipped on a failed / interrupted destroy — those events ARE the post-mortem
70489
+ * the user wants on the retry — and a no-op when `--purge-events` was not
70490
+ * passed.
70491
+ *
70492
+ * "Empty" is bounded, and this used to say "the state bucket returns fully
70493
+ * empty" without the bound (issue
70494
+ * [#2624](https://github.com/go-to-k/cdkd/issues/2624)). The purge routes
70495
+ * through `DeploymentEventsReader.pruneRuns` -> `deleteRawObjects`, which sends
70496
+ * `DeleteObjects` with no `VersionId`: on the versioned state bucket that
70497
+ * writes DELETE MARKERS and leaves every earlier version of those keys readable
70498
+ * with a `VersionId`. The log line below states that rather than claiming a
70499
+ * removal that did not happen.
70496
70500
  *
70497
70501
  * MUST be called AFTER the run's `eventRecorder.finalize()` (which writes this
70498
70502
  * run's own events + index): purging first would just be re-created by the
@@ -70509,7 +70513,7 @@ async function purgeEventsAfterDestroy(reader, stackName, region, opts, logger)
70509
70513
  if (opts.purgeEvents !== true || opts.runResult !== "SUCCEEDED" || opts.interrupted) return null;
70510
70514
  try {
70511
70515
  const purge = await reader.pruneRuns(stackName, region, { all: true });
70512
- if (purge.deletedRunIds.length > 0 || purge.indexDeleted) logger.info(` Purged deployment-event history for ${stackName} (${region}).`);
70516
+ if (purge.deletedRunIds.length > 0 || purge.indexDeleted) logger.info(` Purged deployment-event history for ${stackName} (${region}). Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of those keys survive and stay readable with GetObject and a VersionId.`);
70513
70517
  return purge;
70514
70518
  } catch (purgeError) {
70515
70519
  logger.warn(` Failed to purge deployment-event history for ${stackName}: ${purgeError instanceof Error ? purgeError.message : String(purgeError)}`);
@@ -70793,7 +70797,7 @@ async function destroyCommand(stackArgs, options) {
70793
70797
  * Create destroy command
70794
70798
  */
70795
70799
  function createDestroyCommand() {
70796
- const cmd = new Command("destroy").description("Destroy all resources in the stack").argument("[stacks...]", "Stack name(s) to destroy. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*').").option("--all", "Destroy all stacks", false).option("--purge-events", "After a clean destroy, also delete the stack's deployment-event history (issue #808 store) so the state bucket returns fully empty. By default events survive destroy as post-mortem context. Skipped when the destroy fails or is interrupted (those events aid the retry). Equivalent for an already-destroyed stack: 'cdkd events prune <stack> --all'.", false).action(withErrorHandling(destroyCommand));
70800
+ const cmd = new Command("destroy").description("Destroy all resources in the stack").argument("[stacks...]", "Stack name(s) to destroy. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*').").option("--all", "Destroy all stacks", false).option("--purge-events", "After a clean destroy, also delete the stack's deployment-event history (issue #808 store) so an object listing of the state bucket comes back empty. Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of those keys survive and stay readable with GetObject and a VersionId. By default events survive destroy as post-mortem context. Skipped when the destroy fails or is interrupted (those events aid the retry). Equivalent for an already-destroyed stack: 'cdkd events prune <stack> --all'.", false).action(withErrorHandling(destroyCommand));
70797
70801
  [
70798
70802
  ...commonOptions,
70799
70803
  ...appOptions,
@@ -76201,8 +76205,8 @@ function actionLabel(item, skipFinalSnapshot) {
76201
76205
  case "orphan-retain": return ` - orphan ${op.logicalId} (${op.resourceType}) [DeletionPolicy Retain — left in AWS]`;
76202
76206
  case "orphan-flag": return ` - orphan ${op.logicalId} (${op.resourceType}) [--orphan]`;
76203
76207
  case "revert": return ` - revert ${op.logicalId} (${op.resourceType})${rep}`;
76204
- case "reverse-replacement": return ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-create old resource, delete new]`;
76205
- case "reverse-replacement-readopt": return ` - reverse-replace ${op.logicalId} (${op.resourceType}) [delete new, re-adopt retained old resource]`;
76208
+ case "reverse-replacement": return item.retainsNewResource ? ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-create old resource; new one RETAINED (UpdateReplacePolicy: Retain) and left untracked — REFUSED instead if the re-create collides with the name the retained resource still holds]` : ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-create old resource, delete new]`;
76209
+ case "reverse-replacement-readopt": return item.retainsNewResource ? ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-adopt retained old resource; new one RETAINED (UpdateReplacePolicy: Retain) and left untracked]` : ` - reverse-replace ${op.logicalId} (${op.resourceType}) [delete new, re-adopt retained old resource]`;
76206
76210
  case "unrecoverable-delete": return ` - (cannot restore) ${op.logicalId} (${op.resourceType}) — was DELETED, unrecoverable`;
76207
76211
  case "skip-mismatch": return ` - skip ${op.logicalId} (${op.resourceType}) — physical id changed, needs manual attention`;
76208
76212
  case "skip-absent": return ` - skip ${op.logicalId} (${op.resourceType}) — no longer in state`;
@@ -76538,6 +76542,73 @@ function createRollbackCommand() {
76538
76542
  //#endregion
76539
76543
  //#region src/cli/commands/events.ts
76540
76544
  /**
76545
+ * Issue [#2438](https://github.com/go-to-k/cdkd/issues/2438): every string
76546
+ * `cdkd events` renders on its HUMAN path comes back out of a durable store
76547
+ * (`deployments/{runId}.jsonl` and `deployments/index.json`), and several of
76548
+ * those fields are provider- or template-authored — a logical id, a resource
76549
+ * type, a `reason` that interpolates a user-chosen physical name, an AWS error
76550
+ * message. `JSON.stringify` escapes control bytes on the way IN, so the store
76551
+ * is well-formed, but the reader is a plain `JSON.parse` that restores the
76552
+ * original bytes verbatim. Without a sanitising pass here an ESC-bracket
76553
+ * sequence stored in any of them re-forges the terminal of the one command
76554
+ * whose entire purpose is to be BELIEVED after a run.
76555
+ *
76556
+ * Three helpers rather than one, because the field classes have different
76557
+ * correct answers:
76558
+ *
76559
+ * - {@link safeId} is for values with a KNOWN ASCII charset (a run id, a
76560
+ * logical id, a CFn resource type, a region, an event type, a version
76561
+ * string). `asciiOnly` is a positive allowlist and therefore has no residual
76562
+ * at all — the right fence when nothing legitimate is lost.
76563
+ * - {@link safeText} is for free PROSE (`reason`, `error.name`,
76564
+ * `error.message`). A legitimate AWS error message may carry non-ASCII, and
76565
+ * `asciiOnly` would mangle it — so prose gets the control-character denylist,
76566
+ * accepting `display-safe.ts`'s documented residual.
76567
+ *
76568
+ * The wrap goes around the VALUE, never around the colorized token: sanitising
76569
+ * `red(x)` would strip cdkd's OWN ANSI and leave the row uncolored.
76570
+ */
76571
+ const safeId = (value) => displaySafe(value, { asciiOnly: true });
76572
+ /** Free-prose counterpart of {@link safeId}. See that helper's block comment. */
76573
+ const safeText = (value) => displaySafe(value);
76574
+ /**
76575
+ * Numeric counterpart of {@link safeId}, for the stored COUNTER fields.
76576
+ *
76577
+ * Their declared `number` type is the WRITER's guarantee; the value rendered
76578
+ * here came back through `JSON.parse` and can be any JSON scalar. Sanitising
76579
+ * would be the wrong tool — it would still render the forged text, merely
76580
+ * stripped — so a value that is not a number is not renderable as one and
76581
+ * becomes `?`. Mirrors the pre-existing `typeof e.durationMs === 'number'`
76582
+ * guard, which is why that one line needed no change.
76583
+ */
76584
+ const safeCount = (value) => typeof value === "number" ? String(value) : "?";
76585
+ /**
76586
+ * Appended to every `cdkd events prune` line that reports a DELETE, because
76587
+ * "pruned" and "removed" would otherwise read as removal and are not (issue
76588
+ * [#2624](https://github.com/go-to-k/cdkd/issues/2624)).
76589
+ *
76590
+ * The pruner reaches S3 through `S3StateBackend.deleteRawObjects`, which sends
76591
+ * `DeleteObjects` with no `VersionId`. On the versioned state bucket
76592
+ * `cdkd bootstrap` creates, that writes a DELETE MARKER and leaves every
76593
+ * earlier version of the key readable through `GetObject` with a `VersionId`.
76594
+ * And the versions exist in QUANTITY here rather than as a theoretical
76595
+ * remainder: the store re-PUTs a run's whole JSONL body on every flush, so a
76596
+ * single run accumulates one noncurrent version per flush before it is ever
76597
+ * pruned. `docs/deployment-events.md` classes `deployments/*.jsonl` as
76598
+ * sensitive, which is what makes the distinction worth a line of output.
76599
+ *
76600
+ * Deliberately NOT appended to the "no runs matched" arm: that arm returns
76601
+ * before any `deleteRawObjects` call, so there is no delete to qualify.
76602
+ *
76603
+ * The converse does NOT hold, and the gap is tracked on issue #2624 rather than
76604
+ * closed here: `pruneRuns({ all: true })` reports `indexDeleted: true`
76605
+ * unconditionally (its `DeleteObjects` is idempotent, so it "succeeds" on an
76606
+ * empty prefix), so the arms this note IS appended to can fire for a stack that
76607
+ * had no history at all. Making that gate truthful is a behaviour change, not a
76608
+ * wording one.
76609
+ */
76610
+ const NONCURRENT_VERSIONS_SURVIVE_NOTE = " Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of the deleted keys survive and stay readable with GetObject and a VersionId; prune does not purge them.";
76611
+ /**
76541
76612
  * `cdkd events <stack>` — read back the structured deployment events
76542
76613
  * (issue #808) cdkd persists per deploy / destroy run, the local
76543
76614
  * equivalent of CloudFormation's `DescribeStackEvents`.
@@ -76580,7 +76651,7 @@ async function eventsCommand(stackName, options) {
76580
76651
  const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
76581
76652
  if (options.run) {
76582
76653
  const events = await reader.readRunEvents(stackName, targetRegion, options.run);
76583
- if (events === null) throw new CdkdError(`No deployment-event stream found for run '${options.run}' of stack '${stackName}' in region '${targetRegion}'.`, "EVENTS_RUN_NOT_FOUND");
76654
+ if (events === null) throw new CdkdError(`No deployment-event stream found for run '${safeId(options.run) || "<unrenderable>"}' of stack '${safeId(stackName) || "<unrenderable>"}' in region '${safeId(targetRegion) || "<unrenderable>"}'.`, "EVENTS_RUN_NOT_FOUND");
76584
76655
  if (asJson) {
76585
76656
  process.stdout.write(JSON.stringify(events, null, 2) + "\n");
76586
76657
  return;
@@ -76611,16 +76682,22 @@ async function eventsCommand(stackName, options) {
76611
76682
  async function resolveEventsRegion(reader, stackName, explicitRegion) {
76612
76683
  if (explicitRegion) return explicitRegion;
76613
76684
  const regions = await reader.listRegions(stackName);
76614
- if (regions.length === 0) throw new CdkdError(`No deployment-event history found for stack '${stackName}'. Events are recorded by 'cdkd deploy' / 'cdkd destroy' (issue #808); a stack deployed by an older cdkd version has none.`, "EVENTS_NOT_FOUND");
76615
- if (regions.length > 1) throw new CdkdError(`Stack '${stackName}' has deployment-event history in multiple regions: ${regions.join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
76685
+ if (regions.length === 0) throw new CdkdError(`No deployment-event history found for stack '${safeId(stackName) || "<unrenderable>"}'. Events are recorded by 'cdkd deploy' / 'cdkd destroy' (issue #808); a stack deployed by an older cdkd version has none.`, "EVENTS_NOT_FOUND");
76686
+ if (regions.length > 1) throw new CdkdError(`Stack '${safeId(stackName) || "<unrenderable>"}' has deployment-event history in multiple regions: ${regions.map((r) => safeId(r) || "<unrenderable>").join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
76616
76687
  return regions[0];
76617
76688
  }
76618
76689
  /**
76619
- * `cdkd events prune <stack>` — reclaim S3 space by deleting old per-run
76620
- * `{runId}.jsonl` event streams (issue #885). `cdkd destroy` deliberately
76621
- * keeps event history as post-mortem context, so this is the explicit way
76622
- * to purge it; the deploy/destroy writer also self-bounds to the last
76623
- * {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
76690
+ * `cdkd events prune <stack>` — clear old per-run `{runId}.jsonl` event
76691
+ * streams out of the bucket's object LISTING (issue #885). `cdkd destroy`
76692
+ * deliberately keeps event history as post-mortem context, so this is the
76693
+ * explicit way to purge it; the deploy/destroy writer also self-bounds to the
76694
+ * last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
76695
+ *
76696
+ * It does NOT reclaim the storage, and the wording above said it did until
76697
+ * issue [#2624](https://github.com/go-to-k/cdkd/issues/2624): the delete
76698
+ * carries no `VersionId`, so on a versioned state bucket every earlier
76699
+ * version of a pruned key is still billed and still readable. See
76700
+ * {@link NONCURRENT_VERSIONS_SURVIVE_NOTE}.
76624
76701
  *
76625
76702
  * Retention selection:
76626
76703
  * - `--all` purge every run + the index.
@@ -76657,10 +76734,12 @@ async function eventsPruneCommand(stackName, options) {
76657
76734
  await stateBackend.verifyBucketExists();
76658
76735
  const reader = new DeploymentEventsReader(stateBackend);
76659
76736
  const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
76737
+ const safeStack = safeId(stackName) || "<unrenderable>";
76738
+ const safeRegion = safeId(targetRegion) || "<unrenderable>";
76660
76739
  const totalRuns = (await reader.listRuns(stackName, targetRegion)).length;
76661
- const scope = options.all ? `ALL ${totalRuns} run(s)` : options.keep !== void 0 && olderThanMs !== void 0 ? `runs beyond the newest ${options.keep} AND older than ${options.olderThan}` : options.keep !== void 0 ? `runs beyond the newest ${options.keep}` : olderThanMs !== void 0 ? `runs older than ${options.olderThan}` : `runs beyond the newest ${20}`;
76740
+ const scope = options.all ? `ALL ${totalRuns} run(s)` : options.keep !== void 0 && olderThanMs !== void 0 ? `runs beyond the newest ${options.keep} AND older than ${safeId(options.olderThan)}` : options.keep !== void 0 ? `runs beyond the newest ${options.keep}` : olderThanMs !== void 0 ? `runs older than ${safeId(options.olderThan)}` : `runs beyond the newest ${20}`;
76662
76741
  if (options.yes !== true) {
76663
- if (!await confirmPrompt$2(`Prune deployment-event history for ${cyan(stackName)} ${gray(`(${targetRegion})`)}: ${scope}?`)) {
76742
+ if (!await confirmPrompt$2(`Prune deployment-event history for ${cyan(safeStack)} ${gray(`(${safeRegion})`)}: ${scope}?`)) {
76664
76743
  logger.info(gray("Aborted; nothing was deleted."));
76665
76744
  return;
76666
76745
  }
@@ -76671,10 +76750,10 @@ async function eventsPruneCommand(stackName, options) {
76671
76750
  ...olderThanMs !== void 0 && { olderThanMs }
76672
76751
  });
76673
76752
  if (result.deletedRunIds.length === 0) {
76674
- logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${stackName} (${targetRegion}); no run streams to delete.` : `No runs matched the prune criteria for ${stackName} (${targetRegion}).`));
76753
+ logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${safeStack} (${safeRegion}); no run streams to delete. Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of the deleted keys survive and stay readable with GetObject and a VersionId; prune does not purge them.` : `No runs matched the prune criteria for ${safeStack} (${safeRegion}).`));
76675
76754
  return;
76676
76755
  }
76677
- logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(stackName)} ${gray(`(${targetRegion})`)}; ${result.remainingRunIds.length} retained` + (result.indexDeleted ? gray(" (index removed)") : "") + ".");
76756
+ logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(safeStack)} ${gray(`(${safeRegion})`)}; ${result.remainingRunIds.length} retained` + (result.indexDeleted ? gray(" (index removed)") : "") + "." + gray(NONCURRENT_VERSIONS_SURVIVE_NOTE));
76678
76757
  } finally {
76679
76758
  awsClients.destroy();
76680
76759
  }
@@ -76690,19 +76769,29 @@ async function eventsPruneCommand(stackName, options) {
76690
76769
  async function confirmPrompt$2(prompt) {
76691
76770
  return confirmOrRefuse(prompt, { refusal: "The cdkd events prune confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm the prune, or run the command from a real terminal. Nothing has been deleted." });
76692
76771
  }
76693
- /** Human-readable run listing (newest first). */
76772
+ /**
76773
+ * Human-readable run listing (newest first).
76774
+ *
76775
+ * Issue #2438: every `run.*` field here comes back out of
76776
+ * `deployments/index.json` via a plain `JSON.parse`, so its declared type is a
76777
+ * claim about the writer, not about the bytes on disk — each one goes through
76778
+ * {@link safeId}. `result` is sanitised BEFORE the colour decision so the
76779
+ * colour matches the text the user actually sees.
76780
+ */
76694
76781
  function printRunList(stackName, region, runs) {
76695
76782
  const logger = getLogger();
76696
- logger.info(`${bold("Deployment runs for")} ${cyan(stackName)} ${gray(`(${region})`)}`);
76783
+ const safeStack = safeId(stackName) || "<unrenderable>";
76784
+ logger.info(`${bold("Deployment runs for")} ${cyan(safeStack)} ${gray(`(${safeId(region) || "<unrenderable>"})`)}`);
76697
76785
  if (runs.length === 0) {
76698
76786
  logger.info(gray(" (no runs recorded)"));
76699
76787
  return;
76700
76788
  }
76701
76789
  for (const run of runs) {
76702
- const resultColored = run.result === "SUCCEEDED" ? green(run.result) : run.result === "UNKNOWN" ? gray(run.result) : red(run.result);
76703
- logger.info(` ${cyan(run.runId)} ${run.command} ${resultColored} ${gray(run.startedAt || "?")} -> ${gray(run.finishedAt || "?")} ${gray(`cdkd ${run.cdkdVersion}`)} ${gray(`${run.eventCount} events`)}`);
76790
+ const result = safeId(run.result) || "<unrenderable>";
76791
+ const resultColored = result === "SUCCEEDED" ? green(result) : result === "UNKNOWN" || result === "<unrenderable>" ? gray(result) : red(result);
76792
+ logger.info(` ${cyan(safeId(run.runId) || "<unrenderable>")} ${safeId(run.command) || "<unrenderable>"} ${resultColored} ${gray(safeId(run.startedAt) || "?")} -> ${gray(safeId(run.finishedAt) || "?")} ${gray(`cdkd ${safeId(run.cdkdVersion) || "<unrenderable>"}`)} ${gray(`${safeCount(run.eventCount)} events`)}`);
76704
76793
  }
76705
- logger.info(gray(`\nUse 'cdkd events ${stackName} --run <runId>' to read one run's events.`));
76794
+ logger.info(gray(`\nUse 'cdkd events ${safeStack} --run <runId>' to read one run's events.`));
76706
76795
  }
76707
76796
  /** Human-readable single-run event stream (in recorded order). */
76708
76797
  /**
@@ -76712,29 +76801,38 @@ function printRunList(stackName, region, runs) {
76712
76801
  */
76713
76802
  function printRunEvents(stackName, region, runId, events) {
76714
76803
  const logger = getLogger();
76715
- logger.info(`${bold("Events for run")} ${cyan(runId)} ${gray(`(${stackName}, ${region})`)}`);
76804
+ logger.info(`${bold("Events for run")} ${cyan(safeId(runId) || "<unrenderable>")} ${gray(`(${safeId(stackName) || "<unrenderable>"}, ${safeId(region) || "<unrenderable>"})`)}`);
76716
76805
  if (events.length === 0) {
76717
76806
  logger.info(gray(" (no events)"));
76718
76807
  return;
76719
76808
  }
76720
76809
  for (const e of events) {
76721
- const parts = [gray(e.timestamp), colorizeEventType(e.eventType)];
76722
- if (e.logicalId) parts.push(`${e.logicalId}${e.resourceType ? ` (${e.resourceType})` : ""}`);
76723
- if (e.operation) parts.push(gray(e.operation));
76724
- if (e.guard) parts.push(gray(`guard=${e.guard}`));
76725
- if (e.provisionedBy) parts.push(gray(`[${e.provisionedBy}]`));
76726
- if (e.command) parts.push(gray(e.command));
76727
- if (e.region) parts.push(gray(e.region));
76728
- if (e.cdkdVersion) parts.push(gray(`cdkd ${e.cdkdVersion}`));
76729
- if (e.result) parts.push(e.result === "SUCCEEDED" ? green(e.result) : red(e.result));
76810
+ const parts = [gray(safeId(e.timestamp) || "<unrenderable>"), colorizeEventType(e.eventType)];
76811
+ if (e.logicalId) {
76812
+ const logicalId = safeId(e.logicalId) || "<unrenderable>";
76813
+ const resourceType = e.resourceType ? ` (${safeId(e.resourceType) || "<unrenderable>"})` : "";
76814
+ parts.push(`${logicalId}${resourceType}`);
76815
+ }
76816
+ if (e.operation) parts.push(gray(safeId(e.operation) || "<unrenderable>"));
76817
+ if (e.guard) parts.push(gray(`guard=${safeId(e.guard) || "<unrenderable>"}`));
76818
+ if (e.provisionedBy) parts.push(gray(`[${safeId(e.provisionedBy) || "<unrenderable>"}]`));
76819
+ if (e.command) parts.push(gray(safeId(e.command) || "<unrenderable>"));
76820
+ if (e.region) parts.push(gray(safeId(e.region) || "<unrenderable>"));
76821
+ if (e.cdkdVersion) parts.push(gray(`cdkd ${safeId(e.cdkdVersion) || "<unrenderable>"}`));
76822
+ if (e.result) {
76823
+ const result = safeId(e.result) || "<unrenderable>";
76824
+ parts.push(result === "SUCCEEDED" ? green(result) : result === "<unrenderable>" ? gray(result) : red(result));
76825
+ }
76730
76826
  if (typeof e.durationMs === "number") parts.push(gray(`${e.durationMs}ms`));
76731
- if (e.counts) parts.push(gray(`+${e.counts.created}/~${e.counts.updated}/-${e.counts.deleted}` + (e.counts.failed ? ` !${e.counts.failed}` : "") + (e.counts.skipped ? ` ⚠${e.counts.skipped}` : "")));
76827
+ if (e.counts) parts.push(gray(`+${safeCount(e.counts.created)}/~${safeCount(e.counts.updated)}/-${safeCount(e.counts.deleted)}` + (e.counts.failed ? ` !${safeCount(e.counts.failed)}` : "") + (e.counts.skipped ? ` ⚠${safeCount(e.counts.skipped)}` : "")));
76732
76828
  logger.info(` ${parts.join(" ")}`);
76733
- if (e.reason) logger.info(` ${yellow(e.reason)}`);
76829
+ if (e.reason) logger.info(` ${yellow(safeText(e.reason) || "<unrenderable>")}`);
76734
76830
  if (e.error) {
76735
- const code = e.error.awsErrorCode ? ` (${e.error.awsErrorCode})` : "";
76736
- const reqId = e.error.requestId ? gray(` requestId=${e.error.requestId}`) : "";
76737
- logger.info(` ${red(`${e.error.name}${code}: ${e.error.message}`)}${reqId}`);
76831
+ const code = e.error.awsErrorCode ? ` (${safeId(e.error.awsErrorCode) || "<unrenderable>"})` : "";
76832
+ const reqId = e.error.requestId ? gray(` requestId=${safeId(e.error.requestId) || "<unrenderable>"}`) : "";
76833
+ const name = safeText(e.error.name) || "<unrenderable>";
76834
+ const message = e.error.message ? `: ${safeText(e.error.message) || "<unrenderable>"}` : "";
76835
+ logger.info(` ${red(`${name}${code}${message}`)}${reqId}`);
76738
76836
  }
76739
76837
  }
76740
76838
  }
@@ -76744,14 +76842,22 @@ function printRunEvents(stackName, region, runId, events) {
76744
76842
  * Exported for unit-test coverage (matching `destroy-runner.ts`'s
76745
76843
  * `PROTECTION_PROPERTY_BY_TYPE` convention) — it is pure, so testing it
76746
76844
  * directly beats driving it through the renderer's log output.
76845
+ *
76846
+ * Issue #2438: the declared union is what the WRITER emits; the value handed
76847
+ * here was restored by `JSON.parse` from the stored stream and is therefore
76848
+ * arbitrary text. It is sanitised once, up front, and every arm below both
76849
+ * classifies AND colorizes the sanitised token — classifying the raw value
76850
+ * while printing the sanitised one would let the two disagree. The token is
76851
+ * a known-ASCII discriminator, so `safeId`'s positive allowlist loses nothing.
76747
76852
  */
76748
76853
  function colorizeEventType(eventType) {
76749
- if (eventType.endsWith("FAILED")) return red(eventType);
76750
- if (eventType.endsWith("SUCCEEDED") || eventType === "RUN_FINISHED") return green(eventType);
76751
- if (eventType === "RESOURCE_SKIPPED") return yellow(eventType);
76752
- if (eventType === "RESOURCE_GUARD_INDETERMINATE") return yellow(eventType);
76753
- if (eventType.startsWith("ROLLBACK")) return yellow(eventType);
76754
- return cyan(eventType);
76854
+ const token = safeId(eventType) || "<unrenderable>";
76855
+ if (token.endsWith("FAILED")) return red(token);
76856
+ if (token.endsWith("SUCCEEDED") || token === "RUN_FINISHED") return green(token);
76857
+ if (token === "RESOURCE_SKIPPED") return yellow(token);
76858
+ if (token === "RESOURCE_GUARD_INDETERMINATE") return yellow(token);
76859
+ if (token.startsWith("ROLLBACK")) return yellow(token);
76860
+ return cyan(token);
76755
76861
  }
76756
76862
  /**
76757
76863
  * Create the `cdkd events` command.
@@ -76780,11 +76886,11 @@ function createEventsCommand() {
76780
76886
  * action therefore reads the merged view via `command.optsWithGlobals()`.
76781
76887
  */
76782
76888
  function createEventsPruneCommand() {
76783
- return new Command("prune").description("Delete old per-run deployment-event streams to reclaim S3 space").argument("<stack>", "Stack name (physical CloudFormation name)").addOption(new Option("--keep <N>", "Retain only the newest N runs").argParser((v) => {
76889
+ return new Command("prune").description("Delete old per-run deployment-event streams. On a versioned state bucket this clears the object listing; earlier versions of the deleted keys survive and are not purged.").argument("<stack>", "Stack name (physical CloudFormation name)").addOption(new Option("--keep <N>", "Retain only the newest N runs").argParser((v) => {
76784
76890
  const n = parseInt(v, 10);
76785
- if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${v}": expected a non-negative integer.`);
76891
+ if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${safeId(v)}": expected a non-negative integer.`);
76786
76892
  return n;
76787
- })).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index (full purge)", false).action(withErrorHandling((stack, _options, command) => eventsPruneCommand(stack, command.optsWithGlobals())));
76893
+ })).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index", false).action(withErrorHandling((stack, _options, command) => eventsPruneCommand(stack, command.optsWithGlobals())));
76788
76894
  }
76789
76895
 
76790
76896
  //#endregion
@@ -82182,7 +82288,7 @@ async function pullImage(image, skipPull) {
82182
82288
  try {
82183
82289
  await runDockerForeground(["pull", image]);
82184
82290
  } catch (err) {
82185
- throw new DockerRunnerError(`docker pull ${image} failed: ${err.message}`);
82291
+ throw new DockerRunnerError(`docker pull ${image} failed: ${describeDockerFailure(err, ["pull", image])}`);
82186
82292
  }
82187
82293
  return;
82188
82294
  }
@@ -82191,8 +82297,8 @@ async function pullImage(image, skipPull) {
82191
82297
  await runDockerStreaming(["pull", image], { streamLive: false });
82192
82298
  } catch (err) {
82193
82299
  const e = err;
82194
- if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${image} failed: ${e.message ?? String(err)}`);
82195
- const detail = e.stderr?.trim() || e.stdout?.trim() || "(no output)";
82300
+ if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${image} failed: ${describeDockerFailure(err, ["pull", image])}`);
82301
+ const detail = describeDockerCapturedOutput(err, ["pull", image], "(no output)");
82196
82302
  throw new DockerRunnerError(`docker pull ${image} exited with code ${e.exitCode}: ${detail}`);
82197
82303
  }
82198
82304
  }
@@ -82238,7 +82344,7 @@ async function runDetached(opts) {
82238
82344
  }
82239
82345
  args.push(opts.image, ...entryPointTail, ...opts.cmd);
82240
82346
  const logger = getLogger().child("docker");
82241
- logger.debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
82347
+ logger.debug(`${getDockerCmd()} ${redactDockerArgvValues(args).join(" ")}`);
82242
82348
  if (collisions.length > 0) logger.warn(`Env var(s) ${collisions.map((k) => JSON.stringify(k)).join(", ")} share a name with a docker-client environment variable or have a malformed name (empty, or containing '=' / NUL), and were NOT passed to the container at all (a colliding name would hijack the docker client; a malformed name cannot form a valid environment variable). Rename the env var if the container needs it.`);
82243
82349
  try {
82244
82350
  const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
@@ -82247,8 +82353,7 @@ async function runDetached(opts) {
82247
82353
  });
82248
82354
  return stdout.trim();
82249
82355
  } catch (error) {
82250
- const err = error;
82251
- throw new DockerRunnerError(`docker run failed: ${err.stderr?.trim() || err.message || String(error)}`);
82356
+ throw new DockerRunnerError(`docker run failed: ${describeDockerFailure(error, args)}`);
82252
82357
  }
82253
82358
  }
82254
82359
  /**
@@ -82280,16 +82385,16 @@ function streamLogs(containerId) {
82280
82385
  async function removeContainer(containerId) {
82281
82386
  if (!containerId) return;
82282
82387
  const logger = getLogger().child("docker");
82388
+ const args = [
82389
+ "rm",
82390
+ "-f",
82391
+ containerId
82392
+ ];
82283
82393
  try {
82284
- await execFileAsync$3(getDockerCmd(), [
82285
- "rm",
82286
- "-f",
82287
- containerId
82288
- ]);
82394
+ await execFileAsync$3(getDockerCmd(), args);
82289
82395
  logger.debug(`Removed container ${containerId}`);
82290
82396
  } catch (error) {
82291
- const err = error;
82292
- logger.debug(`docker rm -f ${containerId} failed: ${err.stderr || err.message || String(error)}`);
82397
+ logger.debug(`docker rm -f ${containerId} failed: ${describeDockerFailure(error, args)}`);
82293
82398
  }
82294
82399
  }
82295
82400
  /**
@@ -82299,16 +82404,16 @@ async function removeContainer(containerId) {
82299
82404
  */
82300
82405
  async function ensureDockerAvailable() {
82301
82406
  const cmd = getDockerCmd();
82407
+ const args = [
82408
+ "version",
82409
+ "--format",
82410
+ "{{.Server.Version}}"
82411
+ ];
82302
82412
  try {
82303
- await execFileAsync$3(cmd, [
82304
- "version",
82305
- "--format",
82306
- "{{.Server.Version}}"
82307
- ]);
82413
+ await execFileAsync$3(cmd, args);
82308
82414
  } catch (error) {
82309
- const err = error;
82310
- if (err.code === "ENOENT") throw new DockerRunnerError(`${cmd} is not installed or not on PATH. cdkd local invoke needs Docker (or a compatible CLI specified via CDK_DOCKER) — install it and retry.`);
82311
- throw new DockerRunnerError(`${cmd} daemon is not reachable: ${err.stderr?.trim() || err.message || String(error)}. Start Docker Desktop / the docker daemon and retry.`);
82415
+ if (error.code === "ENOENT") throw new DockerRunnerError(`${cmd} is not installed or not on PATH. cdkd local invoke needs Docker (or a compatible CLI specified via CDK_DOCKER) — install it and retry.`);
82416
+ throw new DockerRunnerError(`${cmd} daemon is not reachable: ${describeDockerFailure(error, args)}. Start Docker Desktop / the docker daemon and retry.`);
82312
82417
  }
82313
82418
  }
82314
82419
  /**
@@ -82339,25 +82444,12 @@ function pickFreePort() {
82339
82444
  });
82340
82445
  }
82341
82446
  /**
82342
- * AWS credential keys whose values must NOT be written to the debug log.
82343
- * `forwardAwsEnv` / `assumeLambdaExecutionRole` (in `local-invoke.ts`)
82344
- * push these via `-e <KEY>=<value>` flags into `runDetached`'s args
82345
- * array, and `cdkd local invoke --verbose` would otherwise leak them
82346
- * into stdout / log files. Only the matching `-e <KEY>=<value>` pair is
82347
- * redacted; non-credential `-e KEY=val` entries pass through unchanged.
82348
- */
82349
- const REDACTED_ENV_KEYS = /* @__PURE__ */ new Set([
82350
- "AWS_ACCESS_KEY_ID",
82351
- "AWS_SECRET_ACCESS_KEY",
82352
- "AWS_SESSION_TOKEN"
82353
- ]);
82354
- /**
82355
82447
  * Built-in sensitive env keys whose VALUES are always routed through
82356
82448
  * docker's value-from-process-env form (`-e KEY` rather than
82357
- * `-e KEY=value`) by {@link runDetached}. Mirrors the redaction-only
82358
- * {@link REDACTED_ENV_KEYS} set above, but stronger: the value is kept
82359
- * off the docker `run` argv (and therefore off `ps` / `/proc/<pid>/cmdline`
82360
- * / verbose debug logs) instead of just being masked at log time. A caller
82449
+ * `-e KEY=value`) by {@link runDetached}. Stronger than any log-time
82450
+ * masking: the value is kept off the docker `run` argv entirely (and
82451
+ * therefore off `ps` / `/proc/<pid>/cmdline` as well as every rendering of
82452
+ * the argv) instead of only being hidden at display time. A caller
82361
82453
  * may extend this set via the `sensitiveEnvKeys` option (used by the
82362
82454
  * AgentCore local-invoke path to keep decrypted `--from-cfn-stack`
82363
82455
  * SecureString SSM values off the argv).
@@ -82367,32 +82459,6 @@ const SENSITIVE_ENV_KEYS = /* @__PURE__ */ new Set([
82367
82459
  "AWS_SECRET_ACCESS_KEY",
82368
82460
  "AWS_SESSION_TOKEN"
82369
82461
  ]);
82370
- /**
82371
- * Returns a copy of `args` with any `-e <KEY>=<value>` pair whose KEY is
82372
- * in {@link REDACTED_ENV_KEYS} replaced with `-e <KEY>=***`. The actual
82373
- * `args` passed to `spawn` are never mutated — this is for log output
82374
- * only.
82375
- */
82376
- function redactAwsCredentialsInArgs(args) {
82377
- const out = [];
82378
- for (let i = 0; i < args.length; i++) {
82379
- const cur = args[i];
82380
- const next = args[i + 1];
82381
- if (cur === "-e" && typeof next === "string") {
82382
- const eqIdx = next.indexOf("=");
82383
- if (eqIdx > 0) {
82384
- const key = next.substring(0, eqIdx);
82385
- if (REDACTED_ENV_KEYS.has(key)) {
82386
- out.push("-e", `${key}=***`);
82387
- i++;
82388
- continue;
82389
- }
82390
- }
82391
- }
82392
- out.push(cur);
82393
- }
82394
- return out;
82395
- }
82396
82462
 
82397
82463
  //#endregion
82398
82464
  //#region src/local/docker-image-builder.ts
@@ -82588,10 +82654,11 @@ async function pullEcrImage(imageUri, options) {
82588
82654
  ecr.destroy();
82589
82655
  }
82590
82656
  logger.info(`Pulling ${canonicalUri}...`);
82657
+ const pullArgs = ["pull", canonicalUri];
82591
82658
  try {
82592
- await runDockerForeground(["pull", canonicalUri]);
82659
+ await runDockerForeground(pullArgs);
82593
82660
  } catch (err) {
82594
- throw new LocalInvokeBuildError$1(`docker pull ${canonicalUri} failed: ${err.message}`);
82661
+ throw new LocalInvokeBuildError$1(`docker pull ${canonicalUri} failed: ${describeDockerFailure(err, pullArgs)}`);
82595
82662
  }
82596
82663
  return canonicalUri;
82597
82664
  }
@@ -82640,17 +82707,17 @@ async function ecrLogin(client, accountId, region) {
82640
82707
  const [username, password] = Buffer.from(authData.authorizationToken, "base64").toString().split(":");
82641
82708
  if (!username || password === void 0) throw new LocalInvokeBuildError$1("ECR authorization token has unexpected shape (missing username/password)");
82642
82709
  const endpoint = authData.proxyEndpoint || `https://${accountId}.dkr.ecr.${region}.${ecrUrlSuffix(region)}`;
82710
+ const loginArgs = [
82711
+ "login",
82712
+ "--username",
82713
+ username,
82714
+ "--password-stdin",
82715
+ endpoint
82716
+ ];
82643
82717
  try {
82644
- await runDockerStreaming([
82645
- "login",
82646
- "--username",
82647
- username,
82648
- "--password-stdin",
82649
- endpoint
82650
- ], { input: password });
82718
+ await runDockerStreaming(loginArgs, { input: password });
82651
82719
  } catch (err) {
82652
- const e = err;
82653
- throw new LocalInvokeBuildError$1(`ECR login failed: ${formatDockerLoginError(e.stderr || e.message || String(err), endpoint)}`);
82720
+ throw new LocalInvokeBuildError$1(`ECR login failed: ${formatDockerLoginError(describeDockerFailure(err, loginArgs), endpoint)}`);
82654
82721
  }
82655
82722
  }
82656
82723
  /**
@@ -85373,19 +85440,19 @@ async function createNetworkAndSidecar(args) {
85373
85440
  const { networkName, cidr, sidecarIp, credentials, cluster, skipPull } = args;
85374
85441
  await pullImage(METADATA_ENDPOINT_IMAGE, skipPull);
85375
85442
  logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
85443
+ const createArgs = [
85444
+ "network",
85445
+ "create",
85446
+ "--driver",
85447
+ "bridge",
85448
+ "--subnet",
85449
+ cidr,
85450
+ networkName
85451
+ ];
85376
85452
  try {
85377
- await execFileAsync$2(getDockerCmd(), [
85378
- "network",
85379
- "create",
85380
- "--driver",
85381
- "bridge",
85382
- "--subnet",
85383
- cidr,
85384
- networkName
85385
- ]);
85453
+ await execFileAsync$2(getDockerCmd(), createArgs);
85386
85454
  } catch (err) {
85387
- const e = err;
85388
- throw new DockerRunnerError(`docker network create failed: ${e.stderr?.trim() || e.message || String(err)}. Hint: another cdkd run may already own subnet ${cidr}; wait for it to finish, or remove the leftover network with \`docker network ls\` + \`docker network rm\`. \`cdkd local start-service\` shares one network across every service in the run; bare \`cdkd local run-task\` uses a per-task network so only one run can be active at a time.`);
85455
+ throw new DockerRunnerError(`docker network create failed: ${describeDockerFailure(err, createArgs)}. Hint: another cdkd run may already own subnet ${cidr}; wait for it to finish, or remove the leftover network with \`docker network ls\` + \`docker network rm\`. \`cdkd local start-service\` shares one network across every service in the run; bare \`cdkd local run-task\` uses a per-task network so only one run can be active at a time.`);
85389
85456
  }
85390
85457
  const sidecarArgs = [
85391
85458
  "run",
@@ -85418,8 +85485,7 @@ async function createNetworkAndSidecar(args) {
85418
85485
  return stdout.trim();
85419
85486
  } catch (err) {
85420
85487
  await destroyNetworkOnly(networkName);
85421
- const e = err;
85422
- throw new DockerRunnerError(`Failed to start metadata-endpoints sidecar: ${e.stderr?.trim() || e.message || String(err)}`);
85488
+ throw new DockerRunnerError(`Failed to start metadata-endpoints sidecar: ${describeDockerFailure(err, sidecarArgs)}`);
85423
85489
  }
85424
85490
  }
85425
85491
  /**
@@ -85481,16 +85547,16 @@ async function destroyTaskNetwork(net) {
85481
85547
  async function destroyNetworkOnly(networkName) {
85482
85548
  if (!networkName) return;
85483
85549
  const logger = getLogger().child("ecs-network");
85550
+ const rmArgs = [
85551
+ "network",
85552
+ "rm",
85553
+ networkName
85554
+ ];
85484
85555
  try {
85485
- await execFileAsync$2(getDockerCmd(), [
85486
- "network",
85487
- "rm",
85488
- networkName
85489
- ]);
85556
+ await execFileAsync$2(getDockerCmd(), rmArgs);
85490
85557
  logger.debug(`Removed docker network ${networkName}`);
85491
85558
  } catch (err) {
85492
- const e = err;
85493
- logger.debug(`docker network rm ${networkName} failed: ${e.stderr || e.message || String(err)}`);
85559
+ logger.debug(`docker network rm ${networkName} failed: ${describeDockerFailure(err, rmArgs)}`);
85494
85560
  }
85495
85561
  }
85496
85562
 
@@ -85666,30 +85732,38 @@ async function cleanupEcsRun(state, options) {
85666
85732
  state.logStoppers = [];
85667
85733
  if (!options.keepRunning) {
85668
85734
  for (const c of state.startedContainers) {
85735
+ const stopArgs = dockerStopArgs(c.id, CLEANUP_STOP_GRACE_SECONDS);
85669
85736
  try {
85670
- await stopContainer(c.id, 10);
85737
+ await stopContainer(c.id, CLEANUP_STOP_GRACE_SECONDS);
85671
85738
  } catch (err) {
85672
- logger.debug(`docker stop ${c.id} failed: ${err instanceof Error ? err.message : String(err)}`);
85739
+ logger.debug(`docker stop ${c.id} failed: ${describeDockerFailure(err, stopArgs)}`);
85673
85740
  }
85674
85741
  try {
85675
85742
  await removeContainer(c.id);
85676
85743
  } catch (err) {
85677
- logger.debug(`docker rm -f ${c.id} failed: ${err instanceof Error ? err.message : String(err)}`);
85744
+ logger.debug(`docker rm -f ${c.id} failed: ${describeDockerFailure(err, [
85745
+ "rm",
85746
+ "-f",
85747
+ c.id
85748
+ ])}`);
85678
85749
  }
85679
85750
  }
85680
85751
  state.startedContainers = [];
85681
85752
  }
85682
85753
  if (state.network && !state.network.ownedByCaller) await destroyTaskNetwork(state.network);
85683
85754
  state.network = void 0;
85684
- for (const v of state.dockerVolumeNames) try {
85685
- await execFileAsync$1(getDockerCmd(), [
85755
+ for (const v of state.dockerVolumeNames) {
85756
+ const rmArgs = [
85686
85757
  "volume",
85687
85758
  "rm",
85688
85759
  v
85689
- ]);
85690
- logger.debug(`Removed docker volume ${v}`);
85691
- } catch (err) {
85692
- logger.debug(`docker volume rm ${v} failed: ${err instanceof Error ? err.message : String(err)}`);
85760
+ ];
85761
+ try {
85762
+ await execFileAsync$1(getDockerCmd(), rmArgs);
85763
+ logger.debug(`Removed docker volume ${v}`);
85764
+ } catch (err) {
85765
+ logger.debug(`docker volume rm ${v} failed: ${describeDockerFailure(err, rmArgs)}`);
85766
+ }
85693
85767
  }
85694
85768
  state.dockerVolumeNames = [];
85695
85769
  }
@@ -85783,8 +85857,7 @@ async function runEcsTask(task, options, state) {
85783
85857
  });
85784
85858
  id = stdout.trim();
85785
85859
  } catch (err) {
85786
- const e = err;
85787
- throw new DockerRunnerError(`docker run failed for container '${container.name}': ${e.stderr?.trim() || e.message || String(err)}`);
85860
+ throw new DockerRunnerError(`docker run failed for container '${container.name}': ${describeDockerFailure(err, args)}`);
85788
85861
  }
85789
85862
  state.startedContainers.push({
85790
85863
  name: container.name,
@@ -85887,14 +85960,15 @@ async function waitForContainerHealthy(containerId, displayName) {
85887
85960
  const logger = getLogger().child("ecs-runner");
85888
85961
  const deadline = Date.now() + 300 * 1e3;
85889
85962
  let lastStatus = "";
85963
+ const inspectArgs = [
85964
+ "inspect",
85965
+ "--format",
85966
+ "{{.State.Health.Status}}",
85967
+ containerId
85968
+ ];
85890
85969
  while (Date.now() < deadline) {
85891
85970
  try {
85892
- const { stdout } = await execFileAsync$1(getDockerCmd(), [
85893
- "inspect",
85894
- "--format",
85895
- "{{.State.Health.Status}}",
85896
- containerId
85897
- ]);
85971
+ const { stdout } = await execFileAsync$1(getDockerCmd(), inspectArgs);
85898
85972
  const status = stdout.trim();
85899
85973
  if (status !== lastStatus) {
85900
85974
  logger.debug(`Container '${displayName}' health status: ${status}`);
@@ -85904,30 +85978,36 @@ async function waitForContainerHealthy(containerId, displayName) {
85904
85978
  if (status === "unhealthy") throw new EcsTaskRunnerError(`Container '${displayName}' health status is 'unhealthy'; aborting before dependents start.`);
85905
85979
  } catch (err) {
85906
85980
  if (err instanceof EcsTaskRunnerError) throw err;
85907
- logger.debug(`docker inspect on '${displayName}' failed: ${err instanceof Error ? err.message : String(err)}`);
85981
+ logger.debug(`docker inspect on '${displayName}' failed: ${describeDockerFailure(err, inspectArgs)}`);
85908
85982
  }
85909
85983
  await sleep(1e3);
85910
85984
  }
85911
85985
  throw new EcsTaskRunnerError(`Container '${displayName}' did not become healthy within 5 minutes.`);
85912
85986
  }
85913
85987
  async function waitForContainerExit(containerId) {
85988
+ const waitArgs = ["wait", containerId];
85914
85989
  try {
85915
- const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
85990
+ const { stdout } = await execFileAsync$1(getDockerCmd(), waitArgs, { maxBuffer: 1024 * 1024 });
85916
85991
  const code = Number.parseInt(stdout.trim(), 10);
85917
85992
  return Number.isFinite(code) ? code : 1;
85918
85993
  } catch (err) {
85919
- const e = err;
85920
- throw new DockerRunnerError(`docker wait failed: ${e.stderr?.trim() || e.message || String(err)}`);
85994
+ throw new DockerRunnerError(`docker wait failed: ${describeDockerFailure(err, waitArgs)}`);
85921
85995
  }
85922
85996
  }
85997
+ /** Seconds `cleanupEcsRun` gives a container to stop before `docker rm -f`. */
85998
+ const CLEANUP_STOP_GRACE_SECONDS = 10;
85999
+ /** The exact `docker stop` argv, so callers redact with the argv actually spawned. */
86000
+ function dockerStopArgs(containerId, graceSeconds) {
86001
+ return [
86002
+ "stop",
86003
+ "-t",
86004
+ String(graceSeconds),
86005
+ containerId
86006
+ ];
86007
+ }
85923
86008
  async function stopContainer(containerId, graceSeconds) {
85924
86009
  try {
85925
- await execFileAsync$1(getDockerCmd(), [
85926
- "stop",
85927
- "-t",
85928
- String(graceSeconds),
85929
- containerId
85930
- ]);
86010
+ await execFileAsync$1(getDockerCmd(), dockerStopArgs(containerId, graceSeconds));
85931
86011
  } catch {}
85932
86012
  }
85933
86013
  function sleep(ms) {
@@ -86009,17 +86089,19 @@ async function prepareOneImage(task, container, options) {
86009
86089
  const actualTag = await buildDockerImage(asset, cdkOutDir, {
86010
86090
  tag,
86011
86091
  ...options.platformOverride !== void 0 && { platform: options.platformOverride },
86012
- wrapError: (stderr) => new LocalInvokeBuildError$1(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable?.join(" ")}): ${stderr}`)
86092
+ wrapError: (stderr) => new LocalInvokeBuildError$1(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? (asset.source.executable ? redactDockerArgvValues(asset.source.executable).join(" ") : void 0)}): ${stderr}`)
86013
86093
  });
86014
- if (actualTag !== tag) try {
86015
- await runDockerStreaming([
86094
+ if (actualTag !== tag) {
86095
+ const tagArgs = [
86016
86096
  "tag",
86017
86097
  actualTag,
86018
86098
  tag
86019
- ]);
86020
- } catch (err) {
86021
- const e = err;
86022
- throw new LocalInvokeBuildError$1(`docker tag failed re-tagging '${actualTag}' '${tag}' for ECS container '${container.name}': ${e.stderr?.trim() || e.message || String(err)}`);
86099
+ ];
86100
+ try {
86101
+ await runDockerStreaming(tagArgs);
86102
+ } catch (err) {
86103
+ throw new LocalInvokeBuildError$1(`docker tag failed re-tagging '${actualTag}' → '${tag}' for ECS container '${container.name}': ${describeDockerFailure(err, tagArgs)}`);
86104
+ }
86023
86105
  }
86024
86106
  return tag;
86025
86107
  }
@@ -86051,8 +86133,7 @@ async function realizeDockerVolumes(volumes, state) {
86051
86133
  state.dockerVolumeNames.push(dockerVolumeName);
86052
86134
  logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
86053
86135
  } catch (err) {
86054
- const e = err;
86055
- throw new DockerRunnerError(`docker volume create failed for '${v.name}': ${e.stderr?.trim() || e.message || String(err)}`);
86136
+ throw new DockerRunnerError(`docker volume create failed for '${v.name}': ${describeDockerFailure(err, args)}`);
86056
86137
  }
86057
86138
  out.set(v.name, {
86058
86139
  ...v,
@@ -86778,40 +86859,37 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86778
86859
  const logger = getLogger();
86779
86860
  const dockerCmd = getDockerCmd();
86780
86861
  let workdir;
86862
+ const inspectArgs = [
86863
+ "inspect",
86864
+ "--format",
86865
+ "{{.Config.WorkingDir}}",
86866
+ containerId
86867
+ ];
86781
86868
  try {
86782
- const { stdout } = await execFileAsync(dockerCmd, [
86783
- "inspect",
86784
- "--format",
86785
- "{{.Config.WorkingDir}}",
86786
- containerId
86787
- ]);
86869
+ const { stdout } = await execFileAsync(dockerCmd, inspectArgs);
86788
86870
  workdir = stdout.trim() || "/";
86789
86871
  } catch (err) {
86790
- throw new CdkdError(`softReloadAgentContainer: docker inspect of container '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_INSPECT_FAILED");
86872
+ throw new CdkdError(`softReloadAgentContainer: docker inspect of container '${containerId}' failed: ${describeDockerExecFailure(err, inspectArgs)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_INSPECT_FAILED");
86791
86873
  }
86792
86874
  const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
86793
86875
  logger.info(`Soft-reload: docker cp ${newAssetSourceDir} -> ${containerId}:${workdirDest}; restart.`);
86876
+ const cpArgs = [
86877
+ "cp",
86878
+ `${newAssetSourceDir}/.`,
86879
+ `${containerId}:${workdirDest}`
86880
+ ];
86794
86881
  try {
86795
- await execFileAsync(dockerCmd, [
86796
- "cp",
86797
- `${newAssetSourceDir}/.`,
86798
- `${containerId}:${workdirDest}`
86799
- ], { maxBuffer: 64 * 1024 * 1024 });
86882
+ await execFileAsync(dockerCmd, cpArgs, { maxBuffer: 64 * 1024 * 1024 });
86800
86883
  } catch (err) {
86801
- throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
86884
+ throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeDockerExecFailure(err, cpArgs)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
86802
86885
  }
86886
+ const restartArgs = ["restart", containerId];
86803
86887
  try {
86804
- await execFileAsync(dockerCmd, ["restart", containerId]);
86888
+ await execFileAsync(dockerCmd, restartArgs);
86805
86889
  } catch (err) {
86806
- throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
86890
+ throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeDockerExecFailure(err, restartArgs)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
86807
86891
  }
86808
86892
  }
86809
- function describeExecError(err) {
86810
- if (!(err instanceof Error)) return String(err);
86811
- const stderr = err.stderr;
86812
- const stderrText = typeof stderr === "string" ? stderr.trim() : stderr instanceof Buffer ? stderr.toString("utf8").trim() : "";
86813
- return stderrText ? `${err.message}\n${stderrText}` : err.message;
86814
- }
86815
86893
  /**
86816
86894
  * Build the per-firing classifier context for the agent container. Mirrors the
86817
86895
  * ECS service emulator's `loadAssetContextForTarget` shape: returns `undefined`
@@ -88049,6 +88127,123 @@ function createLocalStartAgentCoreCommand$1() {
88049
88127
  //#endregion
88050
88128
  //#region src/cli/commands/local-start-alb.ts
88051
88129
  /**
88130
+ * Every distinct Lambda logical id a resolved front-door plan forwards to,
88131
+ * sorted so the warning text is deterministic.
88132
+ *
88133
+ * Walks BOTH action slots — a listener's `defaultAction` and each of its
88134
+ * rules' `action` — because either one can carry a `TargetType: lambda`
88135
+ * target group and an ALB that reaches a Lambda only through a
88136
+ * `path-pattern` rule is the common shape (`/api/*` to a Lambda, everything
88137
+ * else to the ECS service). Mirrors cdk-local's own `collectAlbLambdaTargets`
88138
+ * walk, one layer later: this reads the QUALIFIED plan the strategy returns,
88139
+ * where a Lambda target already carries its resolved `lambda.logicalId`.
88140
+ */
88141
+ function collectLambdaTargetLogicalIds(frontDoor) {
88142
+ if (!frontDoor) return [];
88143
+ const ids = /* @__PURE__ */ new Set();
88144
+ const fromAction = (action) => {
88145
+ if (action?.kind !== "forward") return;
88146
+ for (const target of action.targets) if (target.kind === "lambda") ids.add(target.lambda.logicalId);
88147
+ };
88148
+ for (const listener of frontDoor.listeners) {
88149
+ fromAction(listener.defaultAction);
88150
+ for (const rule of listener.rules) fromAction(rule.action);
88151
+ }
88152
+ return [...ids].sort();
88153
+ }
88154
+ /**
88155
+ * Wrap an ALB {@link EmulatorStrategy} so that, under `--from-state`, a plan
88156
+ * carrying a `TargetType: lambda` target group emits a WARNING naming the
88157
+ * affected Lambdas — because the flag does not reach their container
88158
+ * environment.
88159
+ *
88160
+ * ## Why a warning and not a refusal
88161
+ *
88162
+ * `cdkd local start-cloudfront` REFUSES cdkd's state flags outright
88163
+ * ([#2528](https://github.com/go-to-k/cdkd/issues/2528)) because NO consumer on
88164
+ * that command's path reads a host-registered state source. `start-alb` is
88165
+ * partial, not total, so the same remedy would delete a working capability:
88166
+ *
88167
+ * - **ECS service targets DO honor `--from-state`.** `bootOneTarget` /
88168
+ * `rollOneTarget` (`cdk-local@0.147.7`, `dist/local-studio-BBtUAVNy.js`
88169
+ * `:26474` / `:26422`) hand `createLocalStateProvider` the FULL options bag,
88170
+ * so cdkd's `fromState` factory is selected and the task containers'
88171
+ * images / env / secrets / volumes resolve against S3 state.
88172
+ * - **Lambda target groups do NOT.** `resolveAlbLambdaTargetEnv` (`:26614`)
88173
+ * rebuilds the bag it hands the shared `resolveLambdaContainerEnv` as a
88174
+ * SIX-KEY allow-list — `fromCfnStack` / `assumeRole` / `region` / `profile` /
88175
+ * `stackRegion` / `envVars` (`:26619-26626`) — dropping `fromState` /
88176
+ * `stateBucket` / `statePrefix`. It forwards `extraStateProviders` faithfully,
88177
+ * but the dispatcher activates an extra provider only when `options[key]` is
88178
+ * truthy (`:4894`), and the key it looks for is exactly the one the bag no
88179
+ * longer carries. So cdkd's factory is registered and never selected, and the
88180
+ * Lambda boots with its intrinsics dropped — one WARN per variable, none of
88181
+ * which says the flag was the problem.
88182
+ *
88183
+ * The fix is upstream (two arguments in `resolveAlbLambdaTargetEnv`); until it
88184
+ * lands, this makes the partiality LOUD at boot instead of leaving the user to
88185
+ * read "Environment variable X contains a CloudFormation intrinsic and was
88186
+ * dropped" as a state problem. `--from-cfn-stack` survives the allow-list and
88187
+ * therefore reaches both target kinds, which is what the message points at.
88188
+ *
88189
+ * ## Mechanics
88190
+ *
88191
+ * `runEcsServiceEmulator` logs every string in the `warnings` array
88192
+ * `strategy.resolveBoots` returns, so appending to it is the whole wiring — no
88193
+ * second synth, since the front-door plan is already resolved. Returns the
88194
+ * strategy UNCHANGED when `--from-state` is absent, so the non-state path
88195
+ * carries no wrapper at all.
88196
+ *
88197
+ * The warning REPEATS on every `--watch` reload, because `resolveBoots` is
88198
+ * re-run per reload (`:26264`) and its warnings re-logged. That matches how
88199
+ * cdk-local's own ALB resolution warnings behave on the same path, which is
88200
+ * the whole reason: repeating is what the surrounding output already does.
88201
+ * It is NOT because a reload could introduce a new Lambda target — the reload
88202
+ * discards `frontDoor` and `buildFrontDoor` runs only at boot (`:26110`), so
88203
+ * a Lambda added mid-`--watch` is never stood up at all. An earlier revision
88204
+ * of this comment argued the second, false thing.
88205
+ */
88206
+ function warnUnresolvedLambdaTargetEnv(strategy, options) {
88207
+ if (options.fromState !== true) return strategy;
88208
+ return {
88209
+ ...strategy,
88210
+ resolveBoots: (stacks, chosenTargets) => {
88211
+ const resolved = strategy.resolveBoots(stacks, chosenTargets);
88212
+ const lambdaIds = collectLambdaTargetLogicalIds(resolved.frontDoor);
88213
+ if (lambdaIds.length === 0) return resolved;
88214
+ const ecsNote = resolved.boots.length > 0 ? "The ECS service targets behind this ALB DO honor --from-state. " : "";
88215
+ return {
88216
+ ...resolved,
88217
+ warnings: [...resolved.warnings, `--from-state does not reach the container environment of this ALB's Lambda target group(s): ${lambdaIds.join(", ")}. Their Environment.Variables keep any Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue intrinsics unresolved, and each is then dropped with its own warning. ` + ecsNote + "The only state source the Lambda path reads is --from-cfn-stack <name>, which REPLACES --from-state (the two are mutually exclusive) and reaches both target kinds on a CloudFormation-deployed stack; otherwise override the affected variables with --env-vars. Tracked as go-to-k/cdkd#2602 (upstream go-to-k/cdk-local#707)."]
88218
+ };
88219
+ }
88220
+ };
88221
+ }
88222
+ /**
88223
+ * The `EmulatorStrategy` `cdkd local start-alb` runs with: cdk-local's ALB
88224
+ * strategy, decorated by {@link warnUnresolvedLambdaTargetEnv}.
88225
+ *
88226
+ * Extracted from the command's action for ONE reason, and it is the finding
88227
+ * that produced it: with the composition written inline in the `.action(...)`
88228
+ * closure, deleting the decorator at its only wiring point left the entire
88229
+ * unit suite green (measured in review — 29/29). The decorator's own cases
88230
+ * exercise it directly, so they cannot see a call site that stopped calling
88231
+ * it. A named export gives that seam a subject a test can hold.
88232
+ *
88233
+ * The seam is fenced BEHAVIOURALLY, in
88234
+ * `tests/unit/cli/local-start-alb-wiring.test.ts`, which runs the real command
88235
+ * and drives whatever object the action hands the engine. A source-shape
88236
+ * assertion was tried first and defeated in review round 3 by hoisting
88237
+ * `albStrategy(options)` into a local one statement up: its positive half was
88238
+ * satisfied by the mutation's own COMMENT and its negative half could not
88239
+ * cross the `)` the hoist introduced. That case is deleted rather than
88240
+ * tightened — the behavioural one catches strictly more, and a fence a comment
88241
+ * can satisfy reads as coverage while providing none.
88242
+ */
88243
+ function buildAlbEmulatorStrategy(options) {
88244
+ return warnUnresolvedLambdaTargetEnv(albStrategy(options), options);
88245
+ }
88246
+ /**
88052
88247
  * `cdkl start-alb <Stack/Alb>` — Issue #86 v1. Names an
88053
88248
  * `AWS::ElasticLoadBalancingV2::LoadBalancer`, discovers the ECS service(s)
88054
88249
  * behind its HTTP `forward` listeners, boots their replicas, and stands up a
@@ -88056,8 +88251,8 @@ function createLocalStartAgentCoreCommand$1() {
88056
88251
  * The symmetric ALB counterpart of `start-api`.
88057
88252
  */
88058
88253
  function createLocalStartAlbCommand() {
88059
- const cmd = new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners — by default a cloud-HTTPS listener is served over plain HTTP locally (with X-Forwarded-Proto: https preserved). Pass --tls (or --tls-cert / --tls-key) to terminate TLS locally with a self-signed or user-supplied cert. All six ALB rule-condition fields are honored (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--from-state", "Read cdkd's S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes of the ECS services behind the ALB. Mutually exclusive with --from-cfn-stack.").default(false)).addOption(new Option("--state-bucket <bucket>", "S3 bucket for --from-state. Falls back to CDKD_STATE_BUCKET env or cdk.json context.cdkd.stateBucket.")).addOption(new Option("--state-prefix <prefix>", "S3 key prefix for --from-state state files.").default("cdkd")).action(withErrorHandling(async (targets, options) => {
88060
- await runEcsServiceEmulator(targets, options, albStrategy(options), cdkdExtraStateProviders);
88254
+ const cmd = new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners — by default a cloud-HTTPS listener is served over plain HTTP locally (with X-Forwarded-Proto: https preserved). Pass --tls (or --tls-cert / --tls-key) to terminate TLS locally with a self-signed or user-supplied cert. All six ALB rule-condition fields are honored (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--from-state", "Read cdkd's S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes of the ECS services behind the ALB. Does NOT reach a Lambda target group's container environment (upstream go-to-k/cdk-local#707; a boot warning names the affected functions) — use --from-cfn-stack for those. Mutually exclusive with --from-cfn-stack.").default(false)).addOption(new Option("--state-bucket <bucket>", "S3 bucket for --from-state. Falls back to CDKD_STATE_BUCKET env or cdk.json context.cdkd.stateBucket.")).addOption(new Option("--state-prefix <prefix>", "S3 key prefix for --from-state state files.").default("cdkd")).action(withErrorHandling(async (targets, options) => {
88255
+ await runEcsServiceEmulator(targets, options, buildAlbEmulatorStrategy(options), cdkdExtraStateProviders);
88061
88256
  }));
88062
88257
  addAlbSpecificOptions(cmd);
88063
88258
  return adoptDeprecatedRegionFlag(addCommonEcsServiceOptions(cmd));
@@ -89128,4 +89323,4 @@ function buildProgram() {
89128
89323
 
89129
89324
  //#endregion
89130
89325
  export { buildProgram };
89131
- //# sourceMappingURL=program-BuZzDR4s.js.map
89326
+ //# sourceMappingURL=program-dEYAo80R.js.map