@go-to-k/cdkd 0.284.60 → 0.284.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Ar as isCdkdError, Dr as StateError, Jt as TemplateParser, Mn as Synthesizer, Or as SynthesisError, Sr as ProvisioningError, Ut as DiffCalculator, W as IAMRoleProvider, Yt as LockManager, Zt as S3StateBackend, ar as getAwsClients, cr as AssetError, er as AssemblyReader, fr as DependencyError, ir as AwsClients, jr as normalizeAwsError, kr as formatError, lr as CdkdError, nr as clearBucketRegionCache, or as resetAwsClients, q as ProviderRegistry, qt as DagBuilder, r as DeployEngine, rn as AssetPublisher, rr as resolveBucketRegion, rt as CloudControlProvider, sr as setAwsClients, ur as ConfigError, vr as LockError } from "./deploy-engine-Cc6OYj-d.js";
1
+ import { Ar as isCdkdError, Dr as StateError, Jt as TemplateParser, Mn as Synthesizer, Or as SynthesisError, Sr as ProvisioningError, Ut as DiffCalculator, W as IAMRoleProvider, Yt as LockManager, Zt as S3StateBackend, ar as getAwsClients, cr as AssetError, er as AssemblyReader, fr as DependencyError, ir as AwsClients, jr as normalizeAwsError, kr as formatError, lr as CdkdError, nr as clearBucketRegionCache, or as resetAwsClients, q as ProviderRegistry, qt as DagBuilder, r as DeployEngine, rn as AssetPublisher, rr as resolveBucketRegion, rt as CloudControlProvider, sr as setAwsClients, ur as ConfigError, vr as LockError } from "./deploy-engine-D8AIgkwy.js";
2
2
  import { n as getLogger, r as setLogger, t as ConsoleLogger } from "./logger-zRrlbaQt.js";
3
3
 
4
4
  export { AssemblyReader, AssetError, AssetPublisher, AwsClients, CdkdError, CloudControlProvider, ConfigError, ConsoleLogger, DagBuilder, DependencyError, DeployEngine, DiffCalculator, IAMRoleProvider, LockError, LockManager, ProviderRegistry, ProvisioningError, S3StateBackend, StateError, SynthesisError, Synthesizer, TemplateParser, clearBucketRegionCache, formatError, getAwsClients, getLogger, isCdkdError, normalizeAwsError, resetAwsClients, resolveBucketRegion, setAwsClients, setLogger };
@@ -1,7 +1,7 @@
1
- import { $ as endCommandInterruptScope, $n as derivePartitionAndUrlSuffix, $t as exportNamesCarriedFrom, A as renderStatefulReason, An as AssetManifestLoader, At as isSingleDynamicReferenceToken, B as exportAliasCollisionScrubWarning, Bn as resolveStateBucketWithDefault, Bt as s3BucketRegionalDomainName, C as isFinalSnapshotError, Cn as buildDockerImage, Cr as ResourceTimeoutError, Ct as requireConfigString, D as extractDeploymentEventError, Dn as partitionSensitiveEnv, Dt as createSecretMasker, E as makeCanonicalizePropertiesFn, En as getDockerCmd, Er as StackTerminationProtectionError, Et as TEMPLATE_SOURCED_RULES, F as green, Fn as getLegacyStateBucketName, Fr as isThrottlingError, Ft as classifyReplaySecretRegion, G as collectInlinePolicyNamesManagedBySiblings, Gn as CFN_TEMPLATE_BODY_LIMIT, Gt as describeTypeWithThrottleRetry, H as secretBearingStateKeyWarning, Hn as resolveUseCdkBootstrapAssets, Ht as applyRoleArnIfSet, I as red, In as resolveApp, Ir as markNonRetryable, It as producerRegionsFromState, J as findActionableSilentDrops, Jn as findLargeInlineResources, Jt as TemplateParser, K as clearOnUpdateRemoval, Kn as CFN_TEMPLATE_URL_LIMIT, Kt as withRetry, L as yellow, Ln as resolveAutoAssetStorage, Lt as s3BucketArn, M as bold, Mn as Synthesizer, Mr as withErrorHandling, Mt as maskSecretsInText, N as cyan, Nn as synthesisStatusMessage, Nr as isMarkedNonRetryable, Nt as redactSecretsForState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as runDockerForeground, Ot as dynamicReferenceTokens, P as gray, Pn as getDefaultStateBucketName, Pr as isRetryableTransientError, Pt as scrubResourceRecord, Q as beginCommandInterruptScope, Qn as canonicalizeRegion, Qt as rebuildClientForBucketRegion, R as collectDeclaredOutputNames, Rn as resolveCaptureObservedState, Rt as s3BucketDomainName, S as createPreDeleteFinalSnapshot, Sn as buildDenyExternalAccessPolicy, Sr as ProvisioningError, St as requireConfigObject, T as unsupportedFinalSnapshotError, Tn as formatDockerLoginError, Tr as StackHasActiveImportsError, Tt as STATE_SOURCED_READBACK_RULES, U as stateKeySecretExposure, Un as stateBucketExistenceConfirmed, Ut as DiffCalculator, V as isExportAliasCollision, Vn as resolveStateBucketWithDefaultAndSource, Vt as s3BucketWebsiteUrl, W as IAMRoleProvider, Wn as warnDeprecatedNoPrefixCliFlag, Wt as INTRINSIC_KEYS, X as CUSTOM_RESOURCE_RESPONSE_PREFIX, Xn as expectedOwnerParam, Xt as displaySafe, Y as findSilentDropProperties, Yn as uploadCfnTemplate, Yt as LockManager, Z as DEFAULT_STATE_PREFIX, Zn as PARTITION_TABLE, Zt as S3StateBackend, _ as computeImplicitDeleteEdges, _n as isCrossRegionRedirect, _r as LocalStartServiceError, _t as configBooleanRefusal, a as DeploymentEventsStore, an as WorkGraph, ar as getAwsClients, at as disableInstanceApiTermination, b as buildFinalSnapshotIdentifier, bn as validateAssetBucketName, br as NestedStackChildDirectDestroyError, bt as replayWarn, c as replayFailedOperations, cn as loadPublishableAssetManifest, ct as carriesDynamicReference, d as updatePartialReason, dn as stripControlChars, dr as CrossAccountSecretRefusalError, dt as refStateLookupFromResource, en as importableOutputKeys, er as AssemblyReader, et as interruptWatchListenerCount, f as UNSPECIFIED_SKIP_REASON, fn as AssetModeResolver, ft as WAFv2WebACLProvider, g as IMPLICIT_DELETE_DEPENDENCIES, gn as getBootstrapMarkerKey, gr as LocalMigrateError, gt as coerceCfnBoolean, h as maskingRetryLogger, hn as ensureAssetStorage, hr as LocalInvokeBuildError$1, ht as assertRegionMatch, i as DeploymentEventsReader, in as stringifyValue, ir as AwsClients, it as slowCcOperationTimeoutMs, j as formatResourceLine, jn as getDockerImageBySourceHash, jr as normalizeAwsError, jt as maskSecretsInError, k as isStatefulRecreateTargetSync, kn as runDockerStreaming, kt as errorCauseChain, l as replayRollback, ln as rewriteTemplateAssetReferences, lr as CdkdError, lt as cfnRefValueFromPhysicalId, m as withResourceDeadline, mn as assertAssetBucketRegion, mr as DynamicReferenceRegionAmbiguousError, mt as resolveExplicitPhysicalId, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as shouldRetainResource, nt as startInterruptWatch, o as planFailedOps, on as buildAssetRedirectMap, or as resetAwsClients, ot as isTerminationProtectionPropagationError, p as deleteSkipReason, pn as BOOTSTRAP_MARKER_PREFIX, pr as DeployCancelledError, pt as normalizeAwsTagsToCfn, q as ProviderRegistry, qn as MIGRATE_TMP_PREFIX, qt as DagBuilder, r as DeployEngine, rn as AssetPublisher, rr as resolveBucketRegion, rt as CloudControlProvider, s as planRollback, sn as createAssetRedirectResolver, sr as setAwsClients, st as IntrinsicFunctionResolver, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as importableOutputs, tr as processStackMessages, tt as isInterruptedWaitError, u as updatePartialMessage, un as escapeRegExp, ut as getAccountInfo, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as parseBootstrapMarker, vt as configStringRefusal, w as refusesFinalSnapshot, wn as dockerSpawnEnvWithSensitive, wr as ResourceUpdateNotSupportedError, wt as STATE_SOURCED_CROSS_GENERATION_RULES, x as ccRoutedFinalSnapshotError, xn as validateContainerRepoName, xr as PartialFailureError, xt as requireConfigArray, y as PRE_DELETE_SNAPSHOT_TYPES, yn as readBootstrapMarkerBody, yr as MissingCdkCliError, yt as readConfigString, z as collectPublishedOutputNames, zn as resolveSkipPrefix, zt as s3BucketDualStackDomainName } from "./deploy-engine-Cc6OYj-d.js";
1
+ import { $ as endCommandInterruptScope, $n as derivePartitionAndUrlSuffix, $t as exportNamesCarriedFrom, A as renderStatefulReason, An as AssetManifestLoader, At as isSingleDynamicReferenceToken, B as exportAliasCollisionScrubWarning, Bn as resolveStateBucketWithDefault, Bt as s3BucketRegionalDomainName, C as isFinalSnapshotError, Cn as buildDockerImage, Cr as ResourceTimeoutError, Ct as requireConfigString, D as extractDeploymentEventError, Dn as partitionSensitiveEnv, Dt as createSecretMasker, E as makeCanonicalizePropertiesFn, En as getDockerCmd, Er as StackTerminationProtectionError, Et as TEMPLATE_SOURCED_RULES, F as green, Fn as getLegacyStateBucketName, Fr as isThrottlingError, Ft as classifyReplaySecretRegion, G as collectInlinePolicyNamesManagedBySiblings, Gn as CFN_TEMPLATE_BODY_LIMIT, Gt as describeTypeWithThrottleRetry, H as secretBearingStateKeyWarning, Hn as resolveUseCdkBootstrapAssets, Ht as applyRoleArnIfSet, I as red, In as resolveApp, Ir as markNonRetryable, It as producerRegionsFromState, J as findActionableSilentDrops, Jn as findLargeInlineResources, Jt as TemplateParser, K as clearOnUpdateRemoval, Kn as CFN_TEMPLATE_URL_LIMIT, Kt as withRetry, L as yellow, Ln as resolveAutoAssetStorage, Lt as s3BucketArn, M as bold, Mn as Synthesizer, Mr as withErrorHandling, Mt as maskSecretsInText, N as cyan, Nn as synthesisStatusMessage, Nr as isMarkedNonRetryable, Nt as redactSecretsForState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as runDockerForeground, Ot as dynamicReferenceTokens, P as gray, Pn as getDefaultStateBucketName, Pr as isRetryableTransientError, Pt as scrubResourceRecord, Q as beginCommandInterruptScope, Qn as canonicalizeRegion, Qt as rebuildClientForBucketRegion, R as collectDeclaredOutputNames, Rn as resolveCaptureObservedState, Rt as s3BucketDomainName, S as createPreDeleteFinalSnapshot, Sn as buildDenyExternalAccessPolicy, Sr as ProvisioningError, St as requireConfigObject, T as unsupportedFinalSnapshotError, Tn as formatDockerLoginError, Tr as StackHasActiveImportsError, Tt as STATE_SOURCED_READBACK_RULES, U as stateKeySecretExposure, Un as stateBucketExistenceConfirmed, Ut as DiffCalculator, V as isExportAliasCollision, Vn as resolveStateBucketWithDefaultAndSource, Vt as s3BucketWebsiteUrl, W as IAMRoleProvider, Wn as warnDeprecatedNoPrefixCliFlag, Wt as INTRINSIC_KEYS, X as CUSTOM_RESOURCE_RESPONSE_PREFIX, Xn as expectedOwnerParam, Xt as displaySafe, Y as findSilentDropProperties, Yn as uploadCfnTemplate, Yt as LockManager, Z as DEFAULT_STATE_PREFIX, Zn as PARTITION_TABLE, Zt as S3StateBackend, _ as computeImplicitDeleteEdges, _n as isCrossRegionRedirect, _r as LocalStartServiceError, _t as configBooleanRefusal, a as DeploymentEventsStore, an as WorkGraph, ar as getAwsClients, at as disableInstanceApiTermination, b as buildFinalSnapshotIdentifier, bn as validateAssetBucketName, br as NestedStackChildDirectDestroyError, bt as replayWarn, c as replayFailedOperations, cn as loadPublishableAssetManifest, ct as carriesDynamicReference, d as updatePartialReason, dn as stripControlChars, dr as CrossAccountSecretRefusalError, dt as refStateLookupFromResource, en as importableOutputKeys, er as AssemblyReader, et as interruptWatchListenerCount, f as UNSPECIFIED_SKIP_REASON, fn as AssetModeResolver, ft as WAFv2WebACLProvider, g as IMPLICIT_DELETE_DEPENDENCIES, gn as getBootstrapMarkerKey, gr as LocalMigrateError, gt as coerceCfnBoolean, h as maskingRetryLogger, hn as ensureAssetStorage, hr as LocalInvokeBuildError$1, ht as assertRegionMatch, i as DeploymentEventsReader, in as stringifyValue, ir as AwsClients, it as slowCcOperationTimeoutMs, j as formatResourceLine, jn as getDockerImageBySourceHash, jr as normalizeAwsError, jt as maskSecretsInError, k as isStatefulRecreateTargetSync, kn as runDockerStreaming, kt as errorCauseChain, l as replayRollback, ln as rewriteTemplateAssetReferences, lr as CdkdError, lt as cfnRefValueFromPhysicalId, m as withResourceDeadline, mn as assertAssetBucketRegion, mr as DynamicReferenceRegionAmbiguousError, mt as resolveExplicitPhysicalId, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as shouldRetainResource, nt as startInterruptWatch, o as planFailedOps, on as buildAssetRedirectMap, or as resetAwsClients, ot as isTerminationProtectionPropagationError, p as deleteSkipReason, pn as BOOTSTRAP_MARKER_PREFIX, pr as DeployCancelledError, pt as normalizeAwsTagsToCfn, q as ProviderRegistry, qn as MIGRATE_TMP_PREFIX, qt as DagBuilder, r as DeployEngine, rn as AssetPublisher, rr as resolveBucketRegion, rt as CloudControlProvider, s as planRollback, sn as createAssetRedirectResolver, sr as setAwsClients, st as IntrinsicFunctionResolver, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as importableOutputs, tr as processStackMessages, tt as isInterruptedWaitError, u as updatePartialMessage, un as escapeRegExp, ut as getAccountInfo, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as parseBootstrapMarker, vt as configStringRefusal, w as refusesFinalSnapshot, wn as dockerSpawnEnvWithSensitive, wr as ResourceUpdateNotSupportedError, wt as STATE_SOURCED_CROSS_GENERATION_RULES, x as ccRoutedFinalSnapshotError, xn as validateContainerRepoName, xr as PartialFailureError, xt as requireConfigArray, y as PRE_DELETE_SNAPSHOT_TYPES, yn as readBootstrapMarkerBody, yr as MissingCdkCliError, yt as readConfigString, z as collectPublishedOutputNames, zn as resolveSkipPrefix, zt as s3BucketDualStackDomainName } from "./deploy-engine-D8AIgkwy.js";
2
2
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
3
- import { t as getCdkdVersion } from "./version-Dqo5kOZi.js";
4
- import { t as ASGProvider } from "./asg-provider-D9nlNFCU.js";
3
+ import { t as getCdkdVersion } from "./version-DoW7XkO0.js";
4
+ import { t as ASGProvider } from "./asg-provider-BfX7AwFi.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  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, 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";
@@ -2032,7 +2032,9 @@ function createListCommand() {
2032
2032
  * cancellation, and GitLab CI / `docker stop` / Kubernetes send SIGTERM
2033
2033
  * directly. cdkd's graceful-interrupt handling (the top-level command
2034
2034
  * handlers, the deploy engine's partial-state save, and the per-provider
2035
- * poll-abort listeners in CustomResource / ACM / CloudFront / Route53) is
2035
+ * poll-abort listeners in CustomResource / CloudFront / ACM `grep -rn
2036
+ * "process.on('SIGINT'" src/provisioning/providers/` regenerates that closed
2037
+ * set, and Route53 is NOT in it) is
2036
2038
  * registered on SIGINT only, so an unhandled SIGTERM killed the process
2037
2039
  * mid-run, skipped every `finally`, and stranded the stack lock for its
2038
2040
  * full TTL.
@@ -2082,7 +2084,7 @@ function installSigtermForwarder() {
2082
2084
  * `cdkd deploy` has had one of these since issue #1348 (`deploy.ts`'s
2083
2085
  * `topLevelSigintHandler`). The destroy side had none, so the ONLY channel
2084
2086
  * carrying "the user interrupted" from `runDestroyForStack` back to the
2085
- * `--all` loop was `DestroyRunResult.interrupted` — a boolean assigned ONCE,
2087
+ * `--all` loop was `DestroyRunnerResult.interrupted` — a boolean assigned ONCE,
2086
2088
  * inside the runner's `try`, after its level loop. Every instant where the
2087
2089
  * runner's `draining` flag flips AFTER that assignment is an instant where
2088
2090
  * `cdkd destroy --all` went on to delete the NEXT stack after the user asked
@@ -2106,10 +2108,38 @@ function installSigtermForwarder() {
2106
2108
  *
2107
2109
  * A handler that lives for the whole command closes both, because it is armed
2108
2110
  * in every window rather than in the ones the runner happens to span. The two
2109
- * flags answer different questions and both are kept: `result.interrupted` is
2110
- * the PER-STACK outcome (it decides that stack's state preservation, its
2111
- * summary line and its `--purge-events` skip), while this one is the
2112
- * COMMAND-level "stop" the loop wants.
2111
+ * flags answer different questions and both are kept, one owner each:
2112
+ *
2113
+ * - PER-STACK, "did THIS stack finish, or is there work left in it?" —
2114
+ * `DestroyRunnerResult.interrupted`, owned by `destroy-runner.ts`. It has FOUR
2115
+ * consumers: that stack's state preservation, its summary line,
2116
+ * `destroy.ts`'s `--purge-events` skip, and — the one earlier revisions of
2117
+ * this list omitted — `NestedStackProvider.delete`, which reads the CHILD's
2118
+ * result to decide whether the child row is `{ outcome: 'skipped' }` on the
2119
+ * parent and whether its failure is `markNonRetryable`.
2120
+ *
2121
+ * That fourth consumer is why the `&& statePreserved` gate below is a
2122
+ * behavior change beyond the exit code: a fully-destroyed child that takes
2123
+ * a tail signal no longer reports `interrupted`, so the parent no longer
2124
+ * marks its row skipped. That is the CORRECT reading — `skipped` means
2125
+ * "cdkd could not address this, the resources may still exist in AWS",
2126
+ * which is false of a child whose every resource was deleted and whose
2127
+ * state record is gone; it would preserve the parent's row for a child that
2128
+ * no longer exists and exit 2 over a completed teardown. Stopping the
2129
+ * surrounding `--all` run is a DIFFERENT question, still answered by this
2130
+ * watch, which the parent command holds for its whole run.
2131
+ * - PER-RUN, "has the user asked this COMMAND to stop?" — this watch. Its
2132
+ * consumers are the loops' `break`s and the terminal exit-code verdict
2133
+ * (which pairs it with `stoppedEarly`, so a signal in a completed run's
2134
+ * tail does not report unfinished work there is none of).
2135
+ *
2136
+ * `--purge-events` moved onto the per-stack answer when the runner's outer
2137
+ * re-sync was gated on `statePreserved`. Before that the per-stack flag could
2138
+ * be true over a stack whose state was already DELETED, so the purge had to
2139
+ * read the command-level one to stay conservative — at the cost of the
2140
+ * opposite error: a tail signal on the LAST stack skipped the purge while the
2141
+ * command exited 0, reporting full success with the events left behind. With
2142
+ * the per-stack flag accurate, one read serves both directions.
2113
2143
  *
2114
2144
  * Note on the counterpart contract: `deploy-engine.ts` removes its own SIGINT
2115
2145
  * handler before releasing its lock, which is safe only because `deploy.ts`'s
@@ -2123,6 +2153,11 @@ function installSigtermForwarder() {
2123
2153
  */
2124
2154
  function watchCommandInterrupt(opts) {
2125
2155
  let interrupted = false;
2156
+ const promptAbort = new AbortController();
2157
+ function markInterrupted() {
2158
+ interrupted = true;
2159
+ promptAbort.abort();
2160
+ }
2126
2161
  let stacksInFlight = 0;
2127
2162
  /**
2128
2163
  * Is some OTHER listener going to handle this signal gracefully?
@@ -2145,27 +2180,30 @@ function watchCommandInterrupt(opts) {
2145
2180
  * - `interrupt-watch.ts`'s shared handler, which is a LATCH rather than a
2146
2181
  * graceful owner — deferring to it is exactly the swallow above.
2147
2182
  *
2148
- * A provider's transient wait listener (CustomResource / ACM / CloudFront /
2149
- * Route53) does count, and that is correct rather than a false positive:
2150
- * those exist only DURING a provider wait, which is strictly inside the
2151
- * runner handler's own lifetime, so one cannot be present while the runner's
2152
- * is absent.
2183
+ * A provider's transient wait listener does count, and that is correct
2184
+ * rather than a false positive: three providers install one grep
2185
+ * `process.on('SIGINT'` under `src/provisioning/providers/**` for the
2186
+ * closed set, today `acm-certificate-provider.ts`,
2187
+ * `cloudfront-distribution-provider.ts` and `custom-resource-provider.ts`,
2188
+ * NOT Route53, whose provider registers none — and each exists only
2189
+ * DURING a provider wait, which is strictly inside the runner handler's own
2190
+ * lifetime, so one cannot be present while the runner's is absent.
2153
2191
  */
2154
2192
  function gracefulOwnerArmed() {
2155
2193
  return process.listeners("SIGINT").filter((l) => l !== handler).length - interruptWatchListenerCount() > 0;
2156
2194
  }
2157
2195
  const handler = () => {
2158
2196
  if (stacksInFlight > 0 && gracefulOwnerArmed()) {
2159
- interrupted = true;
2197
+ markInterrupted();
2160
2198
  return;
2161
2199
  }
2162
2200
  if (interrupted) {
2163
- process.stderr.write("\nForce-quit.\n");
2201
+ process.stderr.write("\nForce-quit.\nIf the next run reports a stack lock, release it with: cdkd force-unlock <stack-name>\n");
2164
2202
  process.exit(130);
2165
2203
  }
2166
- interrupted = true;
2204
+ markInterrupted();
2167
2205
  if (stacksInFlight > 0) {
2168
- process.stderr.write(`\nInterrupted before ${opts.command} armed its per-stack teardown — quitting now. Nothing was deleted and no stack lock is held.\n`);
2206
+ process.stderr.write(`\nInterrupted before ${opts.command} armed its per-stack teardown — quitting now. No delete was issued for this stack; on an --all run, stacks processed earlier are already destroyed.\nIf the next run reports a stack lock, release it with: cdkd force-unlock <stack-name>\n`);
2169
2207
  process.exit(130);
2170
2208
  }
2171
2209
  process.stderr.write(`\nInterrupted — ${opts.command} will stop before starting another stack (press Ctrl-C again to force-quit)...\n`);
@@ -2173,6 +2211,7 @@ function watchCommandInterrupt(opts) {
2173
2211
  process.on("SIGINT", handler);
2174
2212
  return {
2175
2213
  interrupted: () => interrupted,
2214
+ signal: promptAbort.signal,
2176
2215
  async runStack(fn) {
2177
2216
  stacksInFlight += 1;
2178
2217
  try {
@@ -2186,6 +2225,22 @@ function watchCommandInterrupt(opts) {
2186
2225
  }
2187
2226
  };
2188
2227
  }
2228
+ /**
2229
+ * Did this rejection come from a prompt aborted through
2230
+ * {@link CommandInterruptWatch.signal}?
2231
+ *
2232
+ * Deliberately duck-typed rather than `instanceof`: what `readline/promises`
2233
+ * rejects with is an internal Node error class in some versions and a
2234
+ * `DOMException` in others, and neither is importable. Both spellings carry
2235
+ * `name === 'AbortError'` / `code === 'ABORT_ERR'`, and the caller pairs this
2236
+ * with its own `watch.interrupted()` check, so a same-named error from an
2237
+ * unrelated abort cannot be mistaken for the user's Ctrl-C.
2238
+ */
2239
+ function isPromptAbortError(error) {
2240
+ if (typeof error !== "object" || error === null) return false;
2241
+ const { name, code } = error;
2242
+ return name === "AbortError" || code === "ABORT_ERR";
2243
+ }
2189
2244
 
2190
2245
  //#endregion
2191
2246
  //#region src/deployment/recreate-targets.ts
@@ -34030,6 +34085,86 @@ const DEPLOYMENT_CONFIG_PRESERVE_KEYS_CAMEL = /* @__PURE__ */ new Set(["hookDeta
34030
34085
  */
34031
34086
  const SERVICE_CONNECT_PRESERVE_KEYS = /* @__PURE__ */ new Set(["Options"]);
34032
34087
  /**
34088
+ * SHARED RATIONALE for both `CIRCUIT_BREAKER_*_DEFAULT` constants below —
34089
+ * WHERE THE VALUES COME FROM, and when they may be sent (issue #1861).
34090
+ *
34091
+ * They are the values CloudFormation was MEASURED to produce when a template
34092
+ * that previously DECLARED the member stops declaring it (us-east-1,
34093
+ * 2026-08-13, same service, `Rollback` flipped in the same call so the update
34094
+ * demonstrably applied and service identity captured on both sides so a
34095
+ * replacement could not be mistaken for a reset): `resetOnHealthyTask` false
34096
+ * -> `true`, `thresholdConfiguration` {COUNT, 7} -> {BOUNDED_PERCENT, 50}.
34097
+ * They match the SDK's documented defaults for both members, and the SDK
34098
+ * documents those unconditionally — they do not vary by scheduling strategy or
34099
+ * deployment type (see the strategy note in `updateService`'s comment block).
34100
+ *
34101
+ * They are used ONLY as `clearOnUpdateRemoval`'s clear value, i.e. only on a
34102
+ * previous-present / current-absent REMOVAL. They must NEVER be sent for a
34103
+ * member the template never declared — a third measurement in the same
34104
+ * session set both members OUT OF BAND on a service whose template never
34105
+ * declared them, then ran a CloudFormation update that flipped `Rollback`
34106
+ * INSIDE the block: the out-of-band `false` / {COUNT, 9} BOTH survived. That
34107
+ * arm is what excludes the competing reading "CFn re-serializes the struct
34108
+ * whenever its declared content changed" and makes this a genuine removal.
34109
+ */
34110
+ /**
34111
+ * `DeploymentCircuitBreaker.ResetOnHealthyTask`'s AWS default. A primitive, so
34112
+ * it needs no copy-on-use treatment — unlike its object-valued sibling below.
34113
+ * Provenance and the send-only-on-removal rule: see the shared note above.
34114
+ */
34115
+ const CIRCUIT_BREAKER_RESET_ON_HEALTHY_TASK_DEFAULT = true;
34116
+ /**
34117
+ * `DeploymentCircuitBreaker.ThresholdConfiguration`'s AWS default. Provenance
34118
+ * and the send-only-on-removal rule: see the shared note above.
34119
+ *
34120
+ * FROZEN, and spread into a fresh object at the use site rather than handed
34121
+ * over by reference. `ECSProvider` is a singleton serving concurrent
34122
+ * resources, so a module-level object reaching the SDK input directly would be
34123
+ * one shared mutable value across every in-flight `UpdateService` — a
34124
+ * middleware or a future normalization step mutating it would corrupt every
34125
+ * other caller. The two halves guard different failures and BOTH are pinned by
34126
+ * unit arms: the copy means no caller can ever reach this object, and the
34127
+ * freeze means a caller that somehow does fails LOUDLY instead of silently
34128
+ * poisoning every later update.
34129
+ *
34130
+ * @internal EXPORTED for tests only (issue #1861). Every production consumer
34131
+ * is in this file. The export exists because the freeze is otherwise
34132
+ * UNOBSERVABLE from a test: the resolver hands out a copy by design, so the
34133
+ * payload a test can reach is never this object, and dropping `Object.freeze`
34134
+ * left the whole suite green. Asserting it through a source grep was the
34135
+ * alternative and is the weaker fence this repo has retired elsewhere.
34136
+ */
34137
+ const CIRCUIT_BREAKER_THRESHOLD_CONFIGURATION_DEFAULT = Object.freeze({
34138
+ type: "BOUNDED_PERCENT",
34139
+ value: 50
34140
+ });
34141
+ /**
34142
+ * A plain object — something a CFn config block can legitimately be. Arrays
34143
+ * and non-objects are excluded on purpose: `typeof [] === 'object'` and every
34144
+ * non-empty string is truthy, so a truthiness check would wave a malformed
34145
+ * template through into a spread.
34146
+ *
34147
+ * Deliberately a module-local copy rather than an export added to
34148
+ * `src/provisioning/config-shape.ts`, whose private `isPlainObject` is
34149
+ * byte-identical: this lane may not widen a shared module's API.
34150
+ *
34151
+ * No "promote it if a third copy appears" trigger, because that already fired
34152
+ * long before this PR and adding it here would read as a new one. Grepped for
34153
+ * a standalone helper whose body is exactly this predicate: NINE across nine
34154
+ * files under six different names — `config-shape.ts`, `wafv2-provider.ts`,
34155
+ * `s3-bucket-provider.ts` (`isPlainObject`), `emr-configuration.ts`,
34156
+ * `lambda-microvm-image-provider.ts` (`isRecord`),
34157
+ * `apigateway-provider.ts` (`isPlainObjectBlock`), `drift.ts`
34158
+ * (`isPlainRecord`), `lambda-vpc-deps.ts` (`isObject`), and this one — plus
34159
+ * further INLINE copies (`route53-provider.ts` x2,
34160
+ * `custom-resource-provider.ts`, `diff-recursive.ts`,
34161
+ * `intrinsic-function-resolver.ts`). Consolidating them touches files across
34162
+ * several lanes and is a separate lane's job, not this PR's.
34163
+ */
34164
+ function isPlainCfnObject(value) {
34165
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34166
+ }
34167
+ /**
34033
34168
  * Derive the cluster name from a long-format ECS Service ARN
34034
34169
  * (`arn:<partition>:ecs:<region>:<account>:service/<clusterName>/<serviceName>`)
34035
34170
  * so `DescribeServices` can be scoped to the right cluster (issue #1170).
@@ -34467,6 +34602,7 @@ var ECSProvider = class {
34467
34602
  const vpcLatticeConfigurationsInput = JSON.stringify(previousProperties["VpcLatticeConfigurations"] ?? null) !== JSON.stringify(properties["VpcLatticeConfigurations"] ?? null) ? clearOnUpdateRemoval(this.convertVpcLatticeConfigurations(properties["VpcLatticeConfigurations"]), previousProperties["VpcLatticeConfigurations"], []) : void 0;
34468
34603
  const monitoringInput = JSON.stringify(previousProperties["Monitoring"] ?? null) !== JSON.stringify(properties["Monitoring"] ?? null) && properties["Monitoring"] != null ? this.convertMonitoringConfiguration(properties["Monitoring"]) : void 0;
34469
34604
  const forceNewDeploymentInput = this.resolveForceNewDeployment(properties, previousProperties);
34605
+ const deploymentConfigurationInput = this.resolveDeploymentConfiguration(properties, previousProperties);
34470
34606
  try {
34471
34607
  const service = (await client.send(new UpdateServiceCommand({
34472
34608
  cluster: properties["Cluster"],
@@ -34475,7 +34611,7 @@ var ECSProvider = class {
34475
34611
  desiredCount: properties["DesiredCount"],
34476
34612
  networkConfiguration: this.convertNetworkConfiguration(properties["NetworkConfiguration"]),
34477
34613
  capacityProviderStrategy: capacityProviderStrategyInput,
34478
- deploymentConfiguration: this.convertDeploymentConfiguration(properties["DeploymentConfiguration"]),
34614
+ deploymentConfiguration: deploymentConfigurationInput,
34479
34615
  placementConstraints: placementConstraintsInput,
34480
34616
  placementStrategy: placementStrategyInput,
34481
34617
  platformVersion: platformVersionInput,
@@ -35187,6 +35323,93 @@ var ECSProvider = class {
35187
35323
  return pascalToCamelCaseKeys(config, DEPLOYMENT_CONFIG_PRESERVE_KEYS);
35188
35324
  }
35189
35325
  /**
35326
+ * UPDATE-path `DeploymentConfiguration` resolver: the verbatim conversion
35327
+ * above, plus the ONE member-level removal reset real AWS measurement found
35328
+ * in this property tree (issue #1861).
35329
+ *
35330
+ * The rule this keys on is previous-present / current-absent — exactly the
35331
+ * semantic `clearOnUpdateRemoval` implements for TOP-LEVEL properties, which
35332
+ * is why this reuses that helper rather than re-spelling the predicate. What
35333
+ * is new is only the DEPTH: the member lives inside a nested struct the
35334
+ * template STILL DECLARES.
35335
+ *
35336
+ * Scope is deliberately narrow, and each boundary is measured rather than
35337
+ * reasoned about (us-east-1, 2026-08-13; the full A/B table is in the long
35338
+ * comment inside `updateService`):
35339
+ * - Only `DeploymentCircuitBreaker`'s two OPTIONAL members
35340
+ * (`ResetOnHealthyTask`, `ThresholdConfiguration`). Its REQUIRED sibling
35341
+ * `Rollback` reads as REPLACED, so one block holds members with
35342
+ * different semantics and no row generalizes to its neighbours.
35343
+ * - NOT the depth-1 siblings `LinearConfiguration` / `CanaryConfiguration`
35344
+ * / `LifecycleHooks[].TimeoutConfiguration`: those are already PARITY
35345
+ * because the ECS API ITSELF replaces the struct and default-fills the
35346
+ * omitted member, so adding a reset there would be the divergence.
35347
+ * - NOT the whole-struct cases. Removing the whole `DeploymentConfiguration`
35348
+ * resets nothing under either engine, and the parent
35349
+ * `DeploymentCircuitBreaker` disappearing entirely was never measured —
35350
+ * so BOTH sides must still declare the block for a member omission to
35351
+ * read as a removal.
35352
+ * - NOT "always send the defaults": a member the template NEVER declared
35353
+ * is left alone by CloudFormation even when the block around it changes,
35354
+ * and `clearOnUpdateRemoval` returning `undefined` for that case is what
35355
+ * keeps cdkd from clobbering an out-of-band value CFn preserves.
35356
+ * - NOT scoped by scheduling strategy, and that is a deliberate omission
35357
+ * rather than an unconsidered one. The A/B ran on a ROLLING / REPLICA /
35358
+ * Fargate service and DAEMON was never exercised, so the question is
35359
+ * fair — and DAEMON is genuinely REACHABLE here, because the SDK states
35360
+ * that CODE_DEPLOY / EXTERNAL controllers do not support DAEMON, so a
35361
+ * DAEMON service necessarily runs the ECS rolling-update type and can
35362
+ * carry a circuit breaker. What makes the omission safe is narrower and
35363
+ * PER-FIELD: the SDK documents these TWO members' defaults with no
35364
+ * strategy dimension. It is not a general property of this block —
35365
+ * `minimumHealthyPercent`'s default IS strategy-dependent (100% replica
35366
+ * vs 0% DAEMON), and stays correct only because cdkd passes it through
35367
+ * rather than defaulting it.
35368
+ *
35369
+ * The previous side stays RAW CFn PascalCase on purpose: `clearOnUpdateRemoval`
35370
+ * only inspects it for PRESENCE, the same shortcut the top-level call sites
35371
+ * in `updateService` take.
35372
+ *
35373
+ * SCOPE OF THAT PRESENCE TEST — state it as what it literally is, because
35374
+ * the obvious reading is stronger than the truth. It means "the previous
35375
+ * BAG carried this member", NOT "the previous TEMPLATE declared it". Those
35376
+ * coincide on the deploy path, where `previousProperties` is the last
35377
+ * deployed template. They do NOT at `drift --revert`
35378
+ * (`src/cli/commands/drift.ts`, the `provider.update(...)` call that passes
35379
+ * `outcome.awsProperties`), where the previous bag is an AWS READBACK: a
35380
+ * member the template never declared but AWS reports is present there, so a
35381
+ * desired side that omits it reads as a removal and gets the default sent.
35382
+ * The window is narrow — the revert path's own desired bag is built from
35383
+ * `observedProperties`, which normally carries the member too, and its
35384
+ * `preserveUntemplated` arm recurses via `mergeUntemplatedValue` and keeps
35385
+ * AWS-only members — but it is real when an `observedProperties` snapshot
35386
+ * predates AWS reporting the member. This is NOT specific to this resolver:
35387
+ * every `clearOnUpdateRemoval` site in the codebase — 78 across 14 provider
35388
+ * files before this PR, 80 with this resolver's two — carries the same
35389
+ * exposure, so fixing it belongs to that shared contract, not here. It is
35390
+ * written down and unit-pinned so the next member copied onto this pattern
35391
+ * does not inherit the premise unchecked.
35392
+ */
35393
+ resolveDeploymentConfiguration(properties, previousProperties) {
35394
+ const desired = this.convertDeploymentConfiguration(properties["DeploymentConfiguration"]);
35395
+ const desiredBreaker = desired?.deploymentCircuitBreaker;
35396
+ if (!desired || !isPlainCfnObject(desiredBreaker)) return desired;
35397
+ const previousDeploymentConfiguration = previousProperties["DeploymentConfiguration"];
35398
+ const previousBreaker = isPlainCfnObject(previousDeploymentConfiguration) ? previousDeploymentConfiguration["DeploymentCircuitBreaker"] : void 0;
35399
+ if (!isPlainCfnObject(previousBreaker)) return desired;
35400
+ const resetOnHealthyTask = clearOnUpdateRemoval(desiredBreaker.resetOnHealthyTask, previousBreaker["ResetOnHealthyTask"], CIRCUIT_BREAKER_RESET_ON_HEALTHY_TASK_DEFAULT);
35401
+ const thresholdConfiguration = clearOnUpdateRemoval(desiredBreaker.thresholdConfiguration, previousBreaker["ThresholdConfiguration"], { ...CIRCUIT_BREAKER_THRESHOLD_CONFIGURATION_DEFAULT });
35402
+ if (resetOnHealthyTask === desiredBreaker.resetOnHealthyTask && thresholdConfiguration === desiredBreaker.thresholdConfiguration) return desired;
35403
+ return {
35404
+ ...desired,
35405
+ deploymentCircuitBreaker: {
35406
+ ...desiredBreaker,
35407
+ ...resetOnHealthyTask !== void 0 ? { resetOnHealthyTask } : {},
35408
+ ...thresholdConfiguration !== void 0 ? { thresholdConfiguration } : {}
35409
+ }
35410
+ };
35411
+ }
35412
+ /**
35190
35413
  * Convert the CFn PascalCase `CapacityProviderStrategy` array to the ECS SDK
35191
35414
  * camelCase input shape (issue #1165). Each element `{CapacityProvider,
35192
35415
  * Weight, Base}` is a pure first-letter case flip to `{capacityProvider,
@@ -59086,7 +59309,6 @@ async function runDestroyForStack(stackName, state, ctx) {
59086
59309
  }
59087
59310
  }
59088
59311
  result.skippedEmpty = true;
59089
- result.interrupted ||= emptyInterrupted;
59090
59312
  return result;
59091
59313
  }
59092
59314
  const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
@@ -59159,6 +59381,7 @@ async function runDestroyForStack(stackName, state, ctx) {
59159
59381
  const renderer = getLiveRenderer();
59160
59382
  let draining = false;
59161
59383
  let lockHeld = false;
59384
+ let statePreserved = false;
59162
59385
  const sigintHandler = () => {
59163
59386
  if (draining) {
59164
59387
  if (lockHeld) {
@@ -59475,6 +59698,7 @@ async function runDestroyForStack(stackName, state, ctx) {
59475
59698
  result.interrupted = draining;
59476
59699
  await saveChain;
59477
59700
  const preserveState = result.errorCount > 0 || result.interrupted || result.skippedCount > 0;
59701
+ statePreserved = preserveState;
59478
59702
  if (!preserveState) {
59479
59703
  await ctx.stateBackend.deleteState(stackName, regionForState);
59480
59704
  logger.debug("State deleted");
@@ -59522,7 +59746,7 @@ async function runDestroyForStack(stackName, state, ctx) {
59522
59746
  process.removeListener("SIGINT", sigintHandler);
59523
59747
  restoreBaseRegionAndClients();
59524
59748
  }
59525
- result.interrupted ||= draining;
59749
+ result.interrupted ||= draining && statePreserved;
59526
59750
  }
59527
59751
  return result;
59528
59752
  }
@@ -68582,6 +68806,7 @@ async function destroyCommand(stackArgs, options) {
68582
68806
  let totalSkipped = 0;
68583
68807
  let interrupted = false;
68584
68808
  const runInterrupted = () => interrupted || interruptWatch.interrupted();
68809
+ let stoppedEarly = false;
68585
68810
  let stackNames;
68586
68811
  if (options.all) stackNames = candidateStacks.map((s) => s.stackName);
68587
68812
  else if (stackPatterns.length > 0) stackNames = matchStacks(candidateStacks, stackPatterns).map((s) => s.stackName);
@@ -68622,7 +68847,7 @@ async function destroyCommand(stackArgs, options) {
68622
68847
  arr.push(ref);
68623
68848
  stateRefsByName.set(ref.stackName, arr);
68624
68849
  }
68625
- for (const stackName of stackNames) {
68850
+ for (const [stackIndex, stackName] of stackNames.entries()) {
68626
68851
  logger.info(`\nPreparing to destroy stack: ${stackName}`);
68627
68852
  const refs = stateRefsByName.get(stackName) ?? [];
68628
68853
  const synthStack = appStacks.find((s) => s.stackName === stackName);
@@ -68658,7 +68883,10 @@ async function destroyCommand(stackArgs, options) {
68658
68883
  totalErrors++;
68659
68884
  continue;
68660
68885
  }
68661
- if (runInterrupted()) break;
68886
+ if (runInterrupted()) {
68887
+ stoppedEarly = true;
68888
+ break;
68889
+ }
68662
68890
  const eventRecorder = startRunRecorder({
68663
68891
  backend: stateBackend,
68664
68892
  stackName,
@@ -68666,6 +68894,8 @@ async function destroyCommand(stackArgs, options) {
68666
68894
  command: "destroy"
68667
68895
  });
68668
68896
  let destroyRunResult = "SUCCEEDED";
68897
+ let stackInterrupted = false;
68898
+ let stackCancelled = false;
68669
68899
  try {
68670
68900
  const result = await interruptWatch.runStack(() => withNestedStackContext({
68671
68901
  stateBackend,
@@ -68709,6 +68939,8 @@ async function destroyCommand(stackArgs, options) {
68709
68939
  })));
68710
68940
  totalErrors += result.errorCount;
68711
68941
  totalSkipped += result.skippedCount;
68942
+ stackInterrupted = result.interrupted;
68943
+ stackCancelled = result.cancelled === true;
68712
68944
  if (result.interrupted) interrupted = true;
68713
68945
  destroyRunResult = result.errorCount > 0 || result.skippedCount > 0 ? "FAILED" : "SUCCEEDED";
68714
68946
  eventRecorder.record({
@@ -68733,12 +68965,15 @@ async function destroyCommand(stackArgs, options) {
68733
68965
  await purgeEventsAfterDestroy(new DeploymentEventsReader(stateBackend), stackName, stackTargetRegion, {
68734
68966
  purgeEvents: options.purgeEvents,
68735
68967
  runResult: destroyRunResult,
68736
- interrupted: runInterrupted()
68968
+ interrupted: stackInterrupted || stackCancelled
68737
68969
  }, logger);
68738
- if (runInterrupted()) break;
68970
+ if (runInterrupted()) {
68971
+ if (stackIndex < stackNames.length - 1) stoppedEarly = true;
68972
+ break;
68973
+ }
68739
68974
  }
68740
68975
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry. If the same resource keeps failing, 'cdkd state orphan <stack>' removes the state record without deleting AWS resources.`);
68741
- if (runInterrupted()) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd destroy' to finish.`);
68976
+ if (interrupted || interruptWatch.interrupted() && stoppedEarly) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd destroy' to finish.`);
68742
68977
  if (totalSkipped > 0) throw new PartialFailureError(`Destroy skipped ${totalSkipped} entr${totalSkipped === 1 ? "y" : "ies"} cdkd could not address, so the underlying resources may still exist in AWS. A skipped nested stack counts as ONE entry and may cover several of its own resources — the per-stack summaries above give the exact breakdown. State preserved (the records are kept). Repair the physicalId in state.json and re-run 'cdkd destroy', or delete the resources by hand and drop the records with 'cdkd state orphan <stack>'.`);
68743
68978
  } finally {
68744
68979
  unforwardSigterm();
@@ -73337,12 +73572,24 @@ async function stateDestroyCommand(stackArgs, options) {
73337
73572
  process.stdout.write(`\nWARNING: This destroys ${stackNames.length} stack(s) and removes their state records:\n`);
73338
73573
  for (const name of stackNames) process.stdout.write(` - ${name}\n`);
73339
73574
  process.stdout.write("\n");
73575
+ if (process.stdin.isTTY !== true) throw new CdkdError("The state destroy --all confirmation prompt cannot run in a non-interactive environment. Pass --yes / -y to confirm the batch, or run the command from a real terminal.", "NON_INTERACTIVE_CONFIRM");
73340
73576
  const rl = readline$1.createInterface({
73341
73577
  input: process.stdin,
73342
73578
  output: process.stdout
73343
73579
  });
73344
- const answer = await rl.question(`Destroy all ${stackNames.length} stack(s)? (y/N): `);
73345
- rl.close();
73580
+ let answer;
73581
+ try {
73582
+ answer = await rl.question(`Destroy all ${stackNames.length} stack(s)? (y/N): `, { signal: interruptWatch.signal });
73583
+ } catch (error) {
73584
+ if (interruptWatch.interrupted() && isPromptAbortError(error)) {
73585
+ process.stderr.write("\nDestroy cancelled — nothing was destroyed.\n");
73586
+ rl.close();
73587
+ process.exit(130);
73588
+ }
73589
+ throw error;
73590
+ } finally {
73591
+ rl.close();
73592
+ }
73346
73593
  const trimmed = answer.trim().toLowerCase();
73347
73594
  if (trimmed !== "y" && trimmed !== "yes") {
73348
73595
  logger.info("Destroy cancelled");
@@ -73354,7 +73601,31 @@ async function stateDestroyCommand(stackArgs, options) {
73354
73601
  let totalSkipped = 0;
73355
73602
  let interrupted = false;
73356
73603
  const runInterrupted = () => interrupted || interruptWatch.interrupted();
73357
- for (const stackName of stackNames) {
73604
+ let stoppedEarly = false;
73605
+ /**
73606
+ * Would any stack AFTER `stackIndex` actually have been destroyed?
73607
+ *
73608
+ * `stackIndex < stackNames.length - 1` answers "does an INDEX remain",
73609
+ * which is not the same question and over-claims: with
73610
+ * `--stack-region us-east-1`, a later name whose only state record lives in
73611
+ * eu-west-1 is warn-and-skipped by the `targets.length === 0` branch below
73612
+ * — it was never going to be destroyed, so a signal before it leaves
73613
+ * nothing undone and the run must still exit 0. Measured on
73614
+ * `state destroy --all --yes --stack-region us-east-1` with StackA in
73615
+ * us-east-1 and StackB only in eu-west-1: the index form exited 2 having
73616
+ * dispatched StackA, i.e. every target it had.
73617
+ *
73618
+ * The predicate mirrors that branch's own filter over `stateRefs`, the
73619
+ * same list the loop selects from. `destroy.ts` needs no counterpart: its
73620
+ * `candidateStacks` is already pre-filtered by the set of stack names that
73621
+ * have state, so an index there IS a target.
73622
+ */
73623
+ const targetRemainsAfter = (stackIndex) => stackNames.slice(stackIndex + 1).some((name) => {
73624
+ const laterRefs = stateRefs.filter((r) => r.stackName === name);
73625
+ if (!options.stackRegion) return laterRefs.length > 0;
73626
+ return laterRefs.some((r) => r.region === options.stackRegion || !r.region);
73627
+ });
73628
+ for (const [stackIndex, stackName] of stackNames.entries()) {
73358
73629
  const refs = stateRefs.filter((r) => r.stackName === stackName);
73359
73630
  let targets;
73360
73631
  if (options.stackRegion) {
@@ -73368,14 +73639,17 @@ async function stateDestroyCommand(stackArgs, options) {
73368
73639
  const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
73369
73640
  throw new Error(`Stack '${stackName}' has state in multiple regions: ${regions}. Use --stack-region <region> to pick one.`);
73370
73641
  }
73371
- for (const ref of targets) {
73642
+ for (const [refIndex, ref] of targets.entries()) {
73372
73643
  logger.info(`\nPreparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`);
73373
73644
  const stateResult = await setup.stateBackend.getState(stackName, ref.region ?? setup.region);
73374
73645
  if (!stateResult) {
73375
73646
  logger.warn(`No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`);
73376
73647
  continue;
73377
73648
  }
73378
- if (runInterrupted()) break;
73649
+ if (runInterrupted()) {
73650
+ stoppedEarly = true;
73651
+ break;
73652
+ }
73379
73653
  const result = await interruptWatch.runStack(() => withNestedStackContext({
73380
73654
  stateBackend: setup.stateBackend,
73381
73655
  lockManager: setup.lockManager,
@@ -73418,12 +73692,18 @@ async function stateDestroyCommand(stackArgs, options) {
73418
73692
  totalErrors += result.errorCount;
73419
73693
  totalSkipped += result.skippedCount;
73420
73694
  if (result.interrupted) interrupted = true;
73421
- if (runInterrupted()) break;
73695
+ if (runInterrupted()) {
73696
+ if (refIndex < targets.length - 1) stoppedEarly = true;
73697
+ break;
73698
+ }
73699
+ }
73700
+ if (runInterrupted()) {
73701
+ if (targetRemainsAfter(stackIndex)) stoppedEarly = true;
73702
+ break;
73422
73703
  }
73423
- if (runInterrupted()) break;
73424
73704
  }
73425
73705
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd state destroy' to retry. If the same resource keeps failing, 'cdkd state orphan <stack>' removes the state record without deleting AWS resources.`);
73426
- if (runInterrupted()) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd state destroy' to finish.`);
73706
+ if (interrupted || interruptWatch.interrupted() && stoppedEarly) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd state destroy' to finish.`);
73427
73707
  if (totalSkipped > 0) throw new PartialFailureError(`Destroy skipped ${totalSkipped} entr${totalSkipped === 1 ? "y" : "ies"} cdkd could not address, so the underlying resources may still exist in AWS. A skipped nested stack counts as ONE entry and may cover several of its own resources — the per-stack summaries above give the exact breakdown. State preserved (the records are kept). Repair the physicalId in state.json and re-run 'cdkd state destroy', or delete the resources by hand and drop the records with 'cdkd state orphan <stack>'.`);
73428
73708
  } finally {
73429
73709
  unforwardSigterm();
@@ -87705,4 +87985,4 @@ function buildProgram() {
87705
87985
 
87706
87986
  //#endregion
87707
87987
  export { buildProgram };
87708
- //# sourceMappingURL=program-BYmBfcxJ.js.map
87988
+ //# sourceMappingURL=program-XLMSMzWe.js.map