@go-to-k/cdkd 0.290.7 → 0.290.8

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 secretSafeKeyDisplay, $n as isCrossRegionRedirect, $r as CdkdError, $t as applyRoleArnIfSet, A as planOrphanAdoption, An as forceQuitRecoveryClause, Ar as resolveStateBucketWithDefaultAndSource, At as isSettledInstanceState, B as formatResourceLine, Bn as shouldRetainResource, Br as expectedOwnerParam, Bt as readConfigString, C as createPreDeleteFinalSnapshot, Ci as isRetryableTransientError, Cn as redactSecretsForState, Cr as getDefaultStateBucketName, Ct as cfnRefValueFromPhysicalId, D as makeCanonicalizePropertiesFn, Di as markRedactedCause, Dn as rebuildClientForBucketRegion, Dr as resolveCaptureObservedState, Dt as isUnboundTemplateParameter, E as unsupportedFinalSnapshotError, Ei as markNonRetryable, En as S3StateBackend, Er as resolveAutoAssetStorage, Et as isImpossibleEmptyStoredAttribute, F as WARM_THROUGHPUT_MEMBERS, Fn as exportNamesCarriedFrom, Fr as CFN_TEMPLATE_URL_LIMIT, Ft as coerceCfnBoolean, G as red, Gn as createAssetRedirectResolver, Gr as processStackMessages, Gt as classifyReplaySecretRegion, H as cyan, Hn as stringifyValue, Hr as canonicalizeRegion, Ht as requireConfigArray, I as coerceWarmThroughput$1, In as importableOutputKeys, Ir as MIGRATE_TMP_PREFIX, It as coerceCfnInteger, J as collectPublishedOutputNames, Jn as AssetModeResolver, Jr as AwsClients, Jt as s3BucketArn, K as yellow, Kn as loadPublishableAssetManifest, Kt as producerRegionsFromState, L as isWarmThroughputDecrease, Ln as importableOutputs, Lr as findLargeInlineResources, Lt as configBooleanRefusal, M as isStatefulRecreateTargetForReplace, Mn as shellQuote, Mr as stateBucketExistenceConfirmed, Mt as normalizeAwsTagsToCfn, N as isStatefulRecreateTargetSync, Nn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Nr as warnDeprecatedNoPrefixCliFlag, Nt as resolveExplicitPhysicalId, O as extractDeploymentEventError, Oi as retryClassificationText, On as buildForceUnlockCommand, Or as resolveSkipPrefix, Ot as parameterTypeMayLoseSecretIdentity, P as renderStatefulReason, Pn as DEFAULT_STATE_PREFIX, Pr as CFN_TEMPLATE_BODY_LIMIT, Pt as assertRegionMatch, Q as secretBearingStateKeyWarning, Qn as getBootstrapMarkerKey, Qt as s3BucketWebsiteUrl, R as toCfnInteger, Rn as orphansAfterRollback, Rr as uploadCfnTemplate, Rt as configIntegerRefusal, S as ccRoutedFinalSnapshotError, Si as isMarkedNonRetryable, Sn as recoverMaskedOutput, Sr as synthesisStatusMessage, St as carriesDynamicReference, T as refusesFinalSnapshot, Ti as isTransientServerError, Tn as LockManager, Tr as resolveApp, Tt as getAccountInfo, U as gray, Un as WorkGraph, Ur as derivePartitionAndUrlSuffix, Ut as requireConfigObject, V as bold, Vn as AssetPublisher, Vr as PARTITION_TABLE, Vt as replayWarn, W as green, Wn as buildAssetRedirectMap, Wr as AssemblyReader, Wt as requireConfigString, X as isExportAliasCollision, Xn as assertAssetBucketRegion, Xr as resetAwsClients, Xt as s3BucketDualStackDomainName, Y as exportAliasCollisionScrubWarning, Yn as BOOTSTRAP_MARKER_PREFIX, Yr as getAwsClients, Yt as s3BucketDomainName, Z as secretBearing, Zn as ensureAssetStorage, Zr as setAwsClients, Zt as s3BucketRegionalDomainName, _ as isSecretBearingReferenceString, _n as isSingleDynamicReferenceToken, _r as escapeRegExp, _t as deleteIndeterminateGuards, a as DeploymentEventsStore, ai as IntrinsicResolutionRefusalError, an as withRetry, ar as describeAwsFailure, at as wouldReturnToSdkProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bi as normalizeAwsError, bn as maskSecretsInText, br as getDockerImageBySourceHash, bt as isTerminationProtectionPropagationError, c as replayFailedOperations, cn as STATE_SOURCED_BASELINE_RULES, cr as describeDockerExecFailure, ct as maskerOrIdentity, d as updatePartialReason, di as ProvisioningError, dn as TEMPLATE_SOURCED_RULES, dr as formatDockerLoginError, dt as interruptWatchListenerCount, en as DiffCalculator, er as parseBootstrapMarker, et as getCurrentResourceSecrets, f as withResourceDeadline, fi as ResourceTimeoutError, fn as carriesSecretMask, fr as getDockerCmd, ft as isInterruptedWaitError, g as bagHoldsSecretExpression, gn as identityKeyFor, gr as runDockerStreaming, gt as UNSPECIFIED_SKIP_REASON, h as computeImplicitDeleteEdges, hi as StackTerminationProtectionError, hn as errorCauseChain, hr as runDockerForeground, ht as slowCcOperationTimeoutMs, i as DeploymentEventsReader, ii as DynamicReferenceRegionAmbiguousError, in as describeTypeWithThrottleRetry, ir as buildDenyExternalAccessPolicy, it as ProviderRegistry, j as MULTI_REGION_RECREATE_BLOCKED_TYPES, jn as formatLockExpiry, jr as resolveUseCdkBootstrapAssets, jt as WAFv2WebACLProvider, k as makeSiblingClaimReader, kn as buildLockContentionMessage, kr as resolveStateBucketWithDefault, kt as refStateLookupFromResource, l as replayRollback, li as NestedStackChildDirectDestroyError, ln as STATE_SOURCED_CROSS_GENERATION_RULES, lr as describeDockerFailure, lt as beginCommandInterruptScope, m as IMPLICIT_DELETE_DEPENDENCIES, mi as StackHasActiveImportsError, mn as dynamicReferenceTokens, mr as redactDockerArgvValues, mt as CloudControlProvider, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as findActionableSilentDrops, nr as validateAssetBucketName, nt as collectInlinePolicyNamesManagedBySiblings, o as planFailedOps, oi as LocalInvokeBuildError$1, on as DagBuilder, or as buildDockerImage, ot as createMaskedRetryLogger, p as maskingRetryLogger, pi as ResourceUpdateNotSupportedError, pn as createSecretMasker, pr as partitionSensitiveEnv, pt as startInterruptWatch, q as collectDeclaredOutputNames, qn as rewriteTemplateAssetReferences, qr as resolveBucketRegion, qt as withSharedDrainBudget, r as DeployEngine, ri as DeployCancelledError, rn as findSilentDropProperties, rr as validateContainerRepoName, rt as clearOnUpdateRemoval, s as planRollback, si as LocalStartServiceError, sn as TemplateParser, sr as describeDockerCapturedOutput, st as maskDeep, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CrossAccountSecretRefusalError, tn as INTRINSIC_KEYS, tr as readBootstrapMarkerBody, tt as IAMRoleProvider, u as updatePartialMessage, ui as PartialFailureError, un as STATE_SOURCED_READBACK_RULES, ur as dockerSpawnEnvWithSensitive, ut as endCommandInterruptScope, v as bindingSkippedOutputs, vn as markSameGenerationBag, vr as stripControlChars, vt as deleteSkipReason, w as isFinalSnapshotError, wi as isThrottlingError, wn as scrubResourceRecord, wr as getLegacyStateBucketName, wt as coerceParameterTypedValue, x as buildFinalSnapshotIdentifier, xi as withErrorHandling, xn as recordMaskOnlyValue, xr as Synthesizer, xt as IntrinsicFunctionResolver, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as maskSecretsInError, yr as AssetManifestLoader, yt as disableInstanceApiTermination, z as toFiniteNumber, zn as orphansCarriedFrom, zr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, zt as configStringRefusal } from "./deploy-engine-BMp4b4Vl.js";
2
- import { S as truncateCodePoints, _ as withSkipPrefix, b as displayIdent, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, x as displaySafe, y as UNRENDERABLE } from "./logger-th1myfgh.js";
1
+ import { $ as secretBearing, $n as ensureAssetStorage, $r as setAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as retryClassificationText, An as buildForceUnlockCommand, Ar as resolveSkipPrefix, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as orphansAfterRollback, Br as uploadCfnTemplate, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Ci as withErrorHandling, Cn as recordMaskOnlyValue, Cr as Synthesizer, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isTransientServerError, Dn as LockManager, Dr as resolveApp, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isThrottlingError, En as scrubResourceRecord, Er as getLegacyStateBucketName, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as warnDeprecatedNoPrefixCliFlag, Ft as resolveExplicitPhysicalId, G as gray, Gn as WorkGraph, Gr as derivePartitionAndUrlSuffix, Gt as requireConfigObject, H as formatResourceLine, Hn as shouldRetainResource, Hr as expectedOwnerParam, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as CFN_TEMPLATE_BODY_LIMIT, It as assertRegionMatch, J as yellow, Jn as loadPublishableAssetManifest, Jt as producerRegionsFromState, K as green, Kn as buildAssetRedirectMap, Kr as AssemblyReader, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as CFN_TEMPLATE_URL_LIMIT, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mn as forceQuitRecoveryClause, Mr as resolveStateBucketWithDefaultAndSource, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveUseCdkBootstrapAssets, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as markNonRetryable, On as S3StateBackend, Or as resolveAutoAssetStorage, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as stateBucketExistenceConfirmed, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as assertAssetBucketRegion, Qr as resetAwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as importableOutputKeys, Rr as MIGRATE_TMP_PREFIX, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Si as normalizeAwsError, Sn as maskSecretsInText, Sr as getDockerImageBySourceHash, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as isRetryableTransientError, Tn as redactSecretsForState, Tr as getDefaultStateBucketName, Tt as cfnRefValueFromPhysicalId, U as bold, Un as AssetPublisher, Ur as PARTITION_TABLE, Ut as replayWarn, V as toFiniteNumber, Vn as orphansCarriedFrom, Vr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Vt as configStringRefusal, W as cyan, Wn as stringifyValue, Wr as canonicalizeRegion, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as AssetModeResolver, Xr as AwsClients, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as rewriteTemplateAssetReferences, Yr as resolveBucketRegion, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as BOOTSTRAP_MARKER_PREFIX, Zr as getAwsClients, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as StackTerminationProtectionError, _n as errorCauseChain, _r as runDockerForeground, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as DeployCancelledError, an as findSilentDropProperties, ar as validateContainerRepoName, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as stripControlChars, bt as deleteSkipReason, c as replayFailedOperations, ci as LocalInvokeBuildError$1, cn as DagBuilder, cr as buildDockerImage, ct as createMaskedRetryLogger, d as updatePartialReason, di as NestedStackChildDirectDestroyError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerFailure, dt as beginCommandInterruptScope, en as s3BucketWebsiteUrl, er as getBootstrapMarkerKey, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as PartialFailureError, fn as STATE_SOURCED_READBACK_RULES, fr as dockerSpawnEnvWithSensitive, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as StackHasActiveImportsError, gn as dynamicReferenceTokens, gr as redactDockerArgvValues, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ResourceUpdateNotSupportedError, hn as createSecretMasker, hr as partitionSensitiveEnv, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as validateAssetBucketName, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, jn as buildLockContentionMessage, jr as resolveStateBucketWithDefault, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as markRedactedCause, kn as rebuildClientForBucketRegion, kr as resolveCaptureObservedState, kt as isUnboundTemplateParameter, l as replayRollback, li as LocalStartServiceError, ln as TemplateParser, lr as describeDockerCapturedOutput, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as ResourceTimeoutError, mn as carriesSecretMask, mr as getDockerCmd, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as parseBootstrapMarker, nt as getCurrentResourceSecrets, o as planFailedOps, oi as DynamicReferenceRegionAmbiguousError, on as describeTypeWithThrottleRetry, or as buildDenyExternalAccessPolicy, ot as ProviderRegistry, p as maskingRetryLogger, pi as ProvisioningError, pn as TEMPLATE_SOURCED_RULES, pr as formatDockerLoginError, pt as interruptWatchListenerCount, q as red, qn as createAssetRedirectResolver, qr as processStackMessages, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CrossAccountSecretRefusalError, rn as INTRINSIC_KEYS, rr as readBootstrapMarkerBody, rt as IAMRoleProvider, s as planRollback, si as IntrinsicResolutionRefusalError, sn as withRetry, sr as describeAwsFailure, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CdkdError, tn as applyRoleArnIfSet, tr as isCrossRegionRedirect, tt as secretSafeKeyDisplay, u as updatePartialMessage, un as STATE_SOURCED_BASELINE_RULES, ur as describeDockerExecFailure, ut as maskerOrIdentity, v as keptWholeReasonText, vn as identityKeyFor, vr as runDockerStreaming, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as isMarkedNonRetryable, wn as recoverMaskedOutput, wr as synthesisStatusMessage, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as AssetManifestLoader, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yn as isSingleDynamicReferenceToken, yr as escapeRegExp, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputs, zr as findLargeInlineResources, zt as configBooleanRefusal } from "./deploy-engine-Bq0TJ9iN.js";
2
+ import { C as truncateCodePoints, S as displaySafe, _ as withSkipPrefix, b as UNRENDERABLE, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, x as displayIdent, y as STACK_REF_MAX_CODE_POINTS } from "./logger-H8n5Qjhq.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-XcHj8rQZ.js";
5
- import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-JRuQeFtl.js";
4
+ import { t as getCdkdVersion } from "./version-CFdjI1Tq.js";
5
+ import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-B4m3magB.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";
@@ -64114,6 +64114,40 @@ function containsSecretDynamicReference(value) {
64114
64114
  function templateHasSecretDynamicReference(template) {
64115
64115
  return containsSecretDynamicReference(template);
64116
64116
  }
64117
+ /**
64118
+ * Can a condition verdict reach an output's VALUE in this template?
64119
+ *
64120
+ * The no-change merge preview (issue #3101) must not carry a failed output a
64121
+ * condition verdict could change, because `cdkd diff` evaluates `Conditions`
64122
+ * best-effort and its verdict can differ from the deploy's. An output never
64123
+ * reads a resource's template properties (a `Ref` or `Fn::GetAtt` reads the
64124
+ * stored record), so a verdict reaches its value only through the output's own
64125
+ * `Condition`, or an `Fn::If` / `{ Condition: ... }` outside `Resources`: in an
64126
+ * output, or in a mapping `Fn::FindInMap` returns. `Conditions` is skipped as
64127
+ * well, since a reference there composes a verdict and reaches an output only
64128
+ * through one of those. A verdict that prunes or keeps a resource differently
64129
+ * changes the resource set instead; where that leaves the diff seeing no change
64130
+ * the deploy sees, the warning naming every carried output already says the
64131
+ * next deploy may write a different value for it.
64132
+ *
64133
+ * Not "the template declares `Conditions`": CDK attaches `CDKMetadataAvailable`
64134
+ * to its `AWS::CDK::Metadata` resource in every env-agnostic stack, and that
64135
+ * test refused the preview for the default app shape.
64136
+ */
64137
+ function templateLetsConditionsReachOutputs(template) {
64138
+ for (const output of Object.values(template.Outputs ?? {})) if (output.Condition !== void 0) return true;
64139
+ return Object.entries(template).some(([section, value]) => section !== "Resources" && section !== "Conditions" && readsConditionVerdict(value));
64140
+ }
64141
+ /** An `Fn::If`, or a `{ Condition: ... }` reference, anywhere under `value`. */
64142
+ function readsConditionVerdict(value) {
64143
+ if (value === null || typeof value !== "object") return false;
64144
+ if (Array.isArray(value)) return value.some(readsConditionVerdict);
64145
+ const record = value;
64146
+ if (Object.prototype.hasOwnProperty.call(record, "Fn::If")) return true;
64147
+ const keys = Object.keys(record);
64148
+ if (keys.length === 1 && keys[0] === "Condition") return true;
64149
+ return Object.values(record).some(readsConditionVerdict);
64150
+ }
64117
64151
  /** Structural equality, mirroring the deploy engine's `outputMapsEqual` leaf rule. */
64118
64152
  function valuesEqual(a, b) {
64119
64153
  if (a === b) return true;
@@ -64193,6 +64227,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64193
64227
  const outputs = Object.create(null);
64194
64228
  const exportNames = /* @__PURE__ */ new Set();
64195
64229
  const failedKeys = /* @__PURE__ */ new Set();
64230
+ const failedOutputKeys = /* @__PURE__ */ new Set();
64231
+ let failuresMirrorDeploy = true;
64196
64232
  const secretSourceKeys = /* @__PURE__ */ new Set();
64197
64233
  const declaredKeys = /* @__PURE__ */ new Set();
64198
64234
  const templateHasSecretReference = containsSecretDynamicReference(template);
@@ -64202,6 +64238,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64202
64238
  outputs,
64203
64239
  exportNames,
64204
64240
  failedKeys,
64241
+ failedOutputKeys,
64242
+ failuresMirrorDeploy,
64205
64243
  secretSourceKeys,
64206
64244
  declaredKeys,
64207
64245
  templateHasSecretReference,
@@ -64226,7 +64264,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64226
64264
  }
64227
64265
  for (const [outputKey, output] of Object.entries(template.Outputs)) {
64228
64266
  if (output.Condition !== void 0 && conditions?.[output.Condition] === false) {
64229
- logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
64267
+ logger.debug(`Skipping output ${stripControlChars(outputKey)} — condition ${stripControlChars(String(output.Condition))} is false`);
64230
64268
  continue;
64231
64269
  }
64232
64270
  if (skippedOutputKeys?.has(outputKey) && !(storedOutputs !== void 0 && Object.prototype.hasOwnProperty.call(storedOutputs, outputKey))) {
@@ -64238,15 +64276,18 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64238
64276
  try {
64239
64277
  value = await resolveFn(structuredClone(output.Value));
64240
64278
  } catch (error) {
64241
- logger.debug(`Diff could not resolve output ${outputKey}: ${String(error)}`);
64279
+ logger.debug(`Diff could not resolve output ${stripControlChars(outputKey)}: ${stripControlChars(String(error))}`);
64242
64280
  resolutionFailed = true;
64243
64281
  recordFailure(outputKey, output);
64282
+ failedOutputKeys.add(outputKey);
64244
64283
  continue;
64245
64284
  }
64246
64285
  if (isUnresolvedValue(value, sourceUsedSub)) {
64247
- logger.debug(`Diff left output ${outputKey} unresolved (references a pending resource)`);
64286
+ logger.debug(`Diff left output ${stripControlChars(outputKey)} unresolved (references a pending resource)`);
64248
64287
  resolutionFailed = true;
64249
64288
  recordFailure(outputKey, output);
64289
+ if (value === void 0) failedOutputKeys.add(outputKey);
64290
+ else failuresMirrorDeploy = false;
64250
64291
  continue;
64251
64292
  }
64252
64293
  outputs[outputKey] = value;
@@ -64257,8 +64298,9 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64257
64298
  if (typeof exportName !== "string") try {
64258
64299
  exportName = await resolveFn(structuredClone(exportName));
64259
64300
  } catch (error) {
64260
- logger.debug(`Diff could not resolve Export.Name of ${outputKey}: ${String(error)}`);
64301
+ logger.debug(`Diff could not resolve Export.Name of ${stripControlChars(outputKey)}: ${stripControlChars(String(error))}`);
64261
64302
  resolutionFailed = true;
64303
+ failuresMirrorDeploy = false;
64262
64304
  continue;
64263
64305
  }
64264
64306
  if (typeof exportName === "string" && !isUnresolvedValue(exportName, exportSourceUsedSub)) {
@@ -64272,18 +64314,24 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64272
64314
  logger.debug(`Diff cannot decide the export alias of ${stripControlChars(outputKey)} — a literal name may contain a resolved secret and state does not hold that key`);
64273
64315
  failedKeys.add(exportName);
64274
64316
  resolutionFailed = true;
64317
+ failuresMirrorDeploy = false;
64275
64318
  }
64276
64319
  } else {
64277
64320
  outputs[exportName] = value;
64278
64321
  exportNames.add(exportName);
64279
64322
  }
64280
- } else resolutionFailed = true;
64323
+ } else {
64324
+ resolutionFailed = true;
64325
+ failuresMirrorDeploy = false;
64326
+ }
64281
64327
  }
64282
64328
  }
64283
64329
  return {
64284
64330
  outputs,
64285
64331
  exportNames,
64286
64332
  failedKeys,
64333
+ failedOutputKeys,
64334
+ failuresMirrorDeploy,
64287
64335
  secretSourceKeys,
64288
64336
  declaredKeys,
64289
64337
  templateHasSecretReference,
@@ -64325,6 +64373,15 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64325
64373
  * Withholding is therefore record-level. The change is still REPORTED — only
64326
64374
  * the value is withheld — so `--fail` and the exports story are unaffected.
64327
64375
  *
64376
+ * A third input forces the same verdict: `forceLegacyRecord`, set by the
64377
+ * no-change merge preview (issue #3101) when a value it carried from state for
64378
+ * a failed output, an export alias included, is not itself a secret expression.
64379
+ * The first signal reads a carried key's desired side, which is then the stored
64380
+ * value, so evidence only its resolved value held is gone; and where a resolved
64381
+ * secret expression can come from (a stored attribute, a parameter, another
64382
+ * stack's outputs) is not enumerable. Nothing short of the carried key's own stored expression can
64383
+ * excuse it — the one value the per-key veto below would have excused.
64384
+ *
64328
64385
  * ## The key today's template cannot account for (issue #1948)
64329
64386
  *
64330
64387
  * Neither signal above covers an output DELETED from the template, and an
@@ -64383,7 +64440,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
64383
64440
  function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /* @__PURE__ */ new Set(), unaccountableScan = {}) {
64384
64441
  const changes = [];
64385
64442
  const currentBag = current ?? {};
64386
- const legacyRecord = Object.entries(currentBag).some(([name, oldValue]) => {
64443
+ const legacyRecord = unaccountableScan.forceLegacyRecord === true || Object.entries(currentBag).some(([name, oldValue]) => {
64387
64444
  if (typeof oldValue === "string" && isSecretBearingReferenceString(oldValue)) return false;
64388
64445
  return containsSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
64389
64446
  });
@@ -65363,8 +65420,28 @@ const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
65363
65420
  * nowhere. The predicate is the plain-object test the bag's own type implies.
65364
65421
  */
65365
65422
  function hasReadableResources(state) {
65366
- const bag = state.resources;
65367
- return typeof bag === "object" && bag !== null && !Array.isArray(bag);
65423
+ return isReadableBag(state.resources);
65424
+ }
65425
+ /**
65426
+ * The plain-object test itself, without the `resources` field bound to it.
65427
+ *
65428
+ * Exported because {@link hasReadableResources} is not the only caller any
65429
+ * more: `cdkd state show` and `cdkd state resources` walk FOUR more record
65430
+ * containers with `Object.entries` — `outputs`, `skippedOutputs`, and each
65431
+ * resource's `attributes` and `properties` — and a non-object in any of them
65432
+ * fabricates rows exactly as the `resources` bag did (go-to-k/cdkd#3187).
65433
+ * Spelling the same test a second time there is what this export exists to
65434
+ * prevent: the two could then drift, and the one that drifted would fabricate
65435
+ * again while looking guarded.
65436
+ *
65437
+ * What it does NOT decide is whether a given container is malformed. That is a
65438
+ * per-container call, because absence means different things: an absent
65439
+ * `resources` bag is a defect ({@link hasReadableResources} says so), while an
65440
+ * absent `skippedOutputs` is the normal pre-#2740 record. Each caller pairs
65441
+ * this predicate with its own absence rule.
65442
+ */
65443
+ function isReadableBag(container) {
65444
+ return typeof container === "object" && container !== null && !Array.isArray(container);
65368
65445
  }
65369
65446
  /**
65370
65447
  * The shared explanation, in the terms the reader needs: what is wrong, what
@@ -65457,6 +65534,65 @@ function repairMalformedResourcesForReadOnly(state) {
65457
65534
  function malformedStateRefusalMessage(stackName, region) {
65458
65535
  return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
65459
65536
  }
65537
+ /**
65538
+ * The warning a view emits when it emptied one or more {@link
65539
+ * RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
65540
+ *
65541
+ * Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
65542
+ * narrower than it looks. That one tells the reader not to run `cdkd deploy` /
65543
+ * `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
65544
+ * from an empty stack: deploy would re-CREATE everything and destroy would
65545
+ * delete nothing. No container named here can produce that specific confusion —
65546
+ * the resource SET is still readable — so borrowing that sentence would attach
65547
+ * a re-create-the-world warning to a record whose resource list is intact.
65548
+ *
65549
+ * It is NOT that these containers are display-only. `properties` is read by the
65550
+ * deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
65551
+ * nothing from this module, and a non-object there compares unequal to any
65552
+ * desired object and yields a spurious property-change set — filed as
65553
+ * go-to-k/cdkd#3191. An earlier revision of this comment asserted the
65554
+ * display-only premise, which would have read as licence to drop a guard on
65555
+ * that path (review of go-to-k/cdkd#3190).
65556
+ *
65557
+ * What stays the same is the remedy: `--json` is the mode that shows the
65558
+ * stored value.
65559
+ *
65560
+ * ONE warning per record however many containers it names — a stack whose 500
65561
+ * resources all carry a hand-edited `properties` gets one line, not 500. The
65562
+ * caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
65563
+ * stable across records. Not `.sort()` — that order is deliberately not
65564
+ * alphabetical, and its own JSDoc says so.
65565
+ *
65566
+ * Both identifiers are sanitized and then shell-quoted, and the command is
65567
+ * emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
65568
+ * gives.
65569
+ *
65570
+ * The container NAMES take {@link safeIdentifier} too — the SAME helper, so
65571
+ * they cannot drift from it — but NOT `shellQuote`, because they appear in the
65572
+ * PROSE and never inside the command this text tells the reader to run; the
65573
+ * command carries the two identifiers and nothing else.
65574
+ *
65575
+ * Sanitizing them is not dead code written for a case that cannot happen. The
65576
+ * union above is closed at COMPILE time, so without this the guarantee would
65577
+ * live in a comment, and the day a caller derives a name from a record instead
65578
+ * of from a literal an unsanitized element could forge a line, render as empty
65579
+ * quotes naming nothing, or run to kilobytes and push the remedy command off
65580
+ * the reader's screen. `safeIdentifier` closes all three and is the identity on
65581
+ * all four literals, so no user-visible text moves.
65582
+ *
65583
+ * What it does NOT close, stated rather than reassured away: a forged name made
65584
+ * only of printable ASCII still renders verbatim inside its quotes and could
65585
+ * read as prose. That residual is bounded to the PROSE — the command the reader
65586
+ * pastes is built from the two shell-quoted identifiers alone — and closing it
65587
+ * would mean JSON-quoting a name in the one place the text is meant to read as
65588
+ * English (review of go-to-k/cdkd#3190).
65589
+ */
65590
+ function malformedRenderedContainersWarning(stackName, region, containers) {
65591
+ const stack = safeIdentifier(stackName);
65592
+ const reg = safeIdentifier(region);
65593
+ const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
65594
+ return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
65595
+ }
65460
65596
  /** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
65461
65597
  function malformedResourcesWarning(stackName, region) {
65462
65598
  return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
@@ -65720,7 +65856,7 @@ async function computeStackDiff(currentState, template, region, stackName, state
65720
65856
  const tokenParameterNames = new Set(Object.entries(parameters ?? {}).filter(([, value]) => carriesDynamicReference(value)).map(([name]) => name));
65721
65857
  for (const name of tokenParameterNames) {
65722
65858
  const declaredType = (template.Parameters?.[name])?.Type;
65723
- if (typeof declaredType === "string" && parameterTypeMayLoseSecretIdentity(declaredType)) logger.warn(`Stack ${stackName}: parameter '${name}' is declared 'Type: ${declaredType}' and is fed a secret dynamic reference. 'cdkd deploy' refuses this when the coercion actually destroys the plaintext (a comma-bearing secret in a list-typed parameter) — declare it 'Type: String' — and this diff compares the unresolved reference, shaped by the declared type only where that keeps every part of it a string.`);
65859
+ if (typeof declaredType === "string" && parameterTypeMayLoseSecretIdentity(declaredType)) logger.warn(`Stack ${stripControlChars(stackName)}: parameter '${stripControlChars(name)}' is declared 'Type: ${stripControlChars(declaredType)}' and is fed a secret dynamic reference. 'cdkd deploy' refuses this when the coercion actually destroys the plaintext (a comma-bearing secret in a list-typed parameter) — declare it 'Type: String' — and this diff compares the unresolved reference, shaped by the declared type only where that keeps every part of it a string.`);
65724
65860
  }
65725
65861
  try {
65726
65862
  const userParameters = {};
@@ -65784,16 +65920,36 @@ async function computeStackDiff(currentState, template, region, stackName, state
65784
65920
  const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
65785
65921
  const bindingSkipped = bindingSkippedOutputs(outputsDigestSource, currentState.skippedOutputs, changedLogicalIds);
65786
65922
  const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs, bindingSkipped);
65787
- let outputChanges = [];
65788
- if (resolved.resolutionFailed) {
65789
- if (computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
65790
- declaredKeys: resolved.declaredKeys,
65791
- templateHasSecretReference: resolved.templateHasSecretReference || inheritSecretBearingTemplate === true
65792
- }).filter((change) => !resolved.failedKeys.has(change.name)).length > 0) logger.warn(`Outputs of stack ${stackName} may have changed, but one or more could not be resolved against current state — omitting the Outputs section from this diff. It is usually an output referencing a resource this deploy has yet to create.`);
65793
- } else outputChanges = computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
65923
+ const templateHasSecretReference = resolved.templateHasSecretReference || inheritSecretBearingTemplate === true;
65924
+ const diffOutputsAgainst = (desired, exportNames, forceLegacyRecord = false) => computeOutputsDiff(currentState.outputs, desired, exportNames, resolved.secretSourceKeys, {
65794
65925
  declaredKeys: resolved.declaredKeys,
65795
- templateHasSecretReference: resolved.templateHasSecretReference || inheritSecretBearingTemplate === true
65926
+ templateHasSecretReference,
65927
+ forceLegacyRecord
65796
65928
  });
65929
+ let outputChanges = [];
65930
+ if (!resolved.resolutionFailed) outputChanges = diffOutputsAgainst(resolved.outputs, resolved.exportNames);
65931
+ else {
65932
+ const resourcesChange = diffCalculator.hasChanges(changes);
65933
+ const merge = resolved.failuresMirrorDeploy && conditions !== void 0 && !templateLetsConditionsReachOutputs(template) && !resourcesChange ? mergeNoChangeOutputs({
65934
+ persisted: currentState.outputs ?? {},
65935
+ resolved: withFailedOutputsUndefined(resolved.outputs, resolved.failedOutputKeys),
65936
+ declaredOutputs: effectiveTemplate.Outputs,
65937
+ previousExportNames: new Set(importableOutputKeys(currentState)),
65938
+ resolvedExportNames: [...resolved.exportNames]
65939
+ }) : void 0;
65940
+ if (merge?.kind === "merged") {
65941
+ const storedOutputs = currentState.outputs ?? {};
65942
+ const withheld = merge.carriedKeys.some((key) => !isSecretBearingReferenceString(storedOutputs[key]));
65943
+ outputChanges = diffOutputsAgainst(merge.outputs, new Set(merge.exportNames), withheld);
65944
+ const failed = [...resolved.failedOutputKeys];
65945
+ const isStored = (key) => Object.prototype.hasOwnProperty.call(storedOutputs, key);
65946
+ const compared = failed.filter(isStored);
65947
+ const unstored = failed.filter((key) => !isStored(key));
65948
+ const names = (keys) => keys.map(stripControlChars).join(", ");
65949
+ const explainWithheld = withheld && outputChanges.some((change) => change.oldValueRedacted === true);
65950
+ logger.warn(`Outputs of stack ${stripControlChars(stackName)}: ${failed.length} output(s) could not be resolved for this diff.` + (compared.length > 0 ? ` Compared at their stored values: ${names(compared)}.` : "") + (unstored.length > 0 ? ` No stored value under their own names: ${names(unstored)}.` : "") + ` The next deploy may write a value for any of them it can resolve.` + (explainWithheld ? ` Previous values in this Outputs section are withheld because a value carried from state for a failed output is not a secret reference, so this diff cannot rule out legacy plaintext in state. That reason no longer applies once every failed output resolves, though other legacy-plaintext checks still can withhold them.` : ""));
65951
+ } else if (diffOutputsAgainst(resolved.outputs, resolved.exportNames).filter((change) => !resolved.failedKeys.has(change.name)).length > 0) logger.warn(`Outputs of stack ${stripControlChars(stackName)} may have changed, but one or more could not be resolved against current state — omitting the Outputs section from this diff. ` + (merge?.kind === "kept" ? keptWholeReasonText(merge.reason) : resourcesChange ? `It is usually an output referencing a resource this deploy has yet to create.` : `The stack has no resource change; this diff cannot preview what the deploy does with the outputs that failed here.`));
65952
+ }
65797
65953
  return {
65798
65954
  changes,
65799
65955
  outputChanges,
@@ -65803,6 +65959,21 @@ async function computeStackDiff(currentState, template, region, stackName, state
65803
65959
  };
65804
65960
  }
65805
65961
  /**
65962
+ * The preview's bag in the shape `mergeNoChangeOutputs` reads from the deploy
65963
+ * engine: each failed OUTPUT present with the value `undefined`
65964
+ * (issue #3101). This resolver drops a failed key while the deploy keeps it,
65965
+ * and the merge tells "failed, keep the stored value" from "not produced,
65966
+ * remove it" by exactly that presence. A fresh null-prototype bag, like both
65967
+ * bags it stands between, so a resolved `__proto__` export alias survives the
65968
+ * copy as a data key.
65969
+ */
65970
+ function withFailedOutputsUndefined(outputs, failedOutputKeys) {
65971
+ const bag = Object.create(null);
65972
+ for (const [key, value] of Object.entries(outputs)) bag[key] = value;
65973
+ for (const key of failedOutputKeys) bag[key] = void 0;
65974
+ return bag;
65975
+ }
65976
+ /**
65806
65977
  * Resolve a nested-stack child's input `Parameters` (declared on the parent's
65807
65978
  * `AWS::CloudFormation::Stack` row under `Properties.Parameters`) to scalar
65808
65979
  * values against the PARENT's deployed state + already-resolved parameters.
@@ -66312,7 +66483,7 @@ function renderOutputChangeLines(outputChanges, logFn) {
66312
66483
  */
66313
66484
  function renderDiffTree(node, isRoot, logFn) {
66314
66485
  const hasChanges = nodeHasChanges(node);
66315
- if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
66486
+ if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${stripControlChars(node.stackName)}:` : `\nNested stack: ${stripControlChars(node.displayName)}`);
66316
66487
  if (hasChanges) {
66317
66488
  const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes, node.adoptedOrphans);
66318
66489
  const outputs = renderOutputChangeLines(node.outputChanges, logFn);
@@ -75430,7 +75601,13 @@ async function buildCdkdStateStackTree(rootStackName, region, stateBackend, pref
75430
75601
  }
75431
75602
  async function walkCdkdStateStackTree(stackName, region, state, stateBackend) {
75432
75603
  const nestedChildren = /* @__PURE__ */ new Map();
75433
- for (const [logicalId, entry] of Object.entries(state.resources ?? {})) {
75604
+ if (!hasReadableResources(state)) return {
75605
+ stackName,
75606
+ region,
75607
+ state,
75608
+ nestedChildren
75609
+ };
75610
+ for (const [logicalId, entry] of Object.entries(state.resources)) {
75434
75611
  if (entry?.resourceType !== "AWS::CloudFormation::Stack") continue;
75435
75612
  const childStackName = `${stackName}~${logicalId}`;
75436
75613
  const childResult = await stateBackend.getState(childStackName, region);
@@ -77556,14 +77733,74 @@ function isoTimestampOrNull(value) {
77556
77733
  * PROMPT is strictly worse than one in an error, because the sentence it
77557
77734
  * corrupts is the one the operator answers `y` to.
77558
77735
  *
77559
- * `asciiOnly` matches the lock error's own call on these same values — a
77560
- * stack name and an AWS region both have a known charset — and is a no-op on
77561
- * every ordinary input, so the shipped prompt strings are byte-identical.
77562
- * `UNRENDERABLE` stands in when sanitising leaves nothing, since an empty
77563
- * `()` would read as "no region" rather than "a region cdkd will not print".
77736
+ * BOTH halves go through `displayIdent`, not the bare `safe()` allowlist below
77737
+ * (issue #3164). The allowlist closes line FORGERY and nothing else: it is the
77738
+ * identity on printable ASCII, and this function's ` (` / `)` is cdkd's OWN
77739
+ * annotation of the line, so a planted 2-segment legacy key
77740
+ * `cdkd/ProdStack (us-east-1)/state.json` yields a region-LESS ref whose
77741
+ * `stackName` is literally `ProdStack (us-east-1)` and renders BYTE-EQUAL to
77742
+ * the genuine `ProdStack` in `us-east-1`. A `while read -r ref` cleanup loop —
77743
+ * the consumer the sanitization exists for — cannot tell the two apart, so it
77744
+ * acts on the reference the operator believes is real and the planted record
77745
+ * survives the sweep. The join in the two prompt callers is a second boundary
77746
+ * of the same kind: they `join(', ')`, which a name carrying `, ` forges an
77747
+ * extra entry in -- and quoting closes that, since a space is outside
77748
+ * `PLAIN_IDENT`.
77749
+ *
77750
+ * A BARE `,` is NOT closed, and an earlier revision of this comment was wrong
77751
+ * to call it harmless: the formatter supplies the space that completes the
77752
+ * separator, so `ProdStack,` renders `ProdStack, (us-east-1)` and a two-target
77753
+ * list reads as THREE entries. Only `state refresh-observed` prints a count
77754
+ * beside its list; `state orphan`'s banner prints none, so there nothing on
77755
+ * screen contradicts the forged entry -- the worse of the two. Removing `,`
77756
+ * from `PLAIN_IDENT` would close it and was tried; it regresses a legitimate
77757
+ * IAM role ARN, whose role-name segment allows `[\w+=,.@-]`. Recorded on
77758
+ * go-to-k/cdkd#3179 rather than traded for that.
77759
+ *
77760
+ * `displayIdent` makes the boundary VISIBLE by JSON-quoting anything that is
77761
+ * not a plain identifier, so the row above reads `"ProdStack (us-east-1)"` and
77762
+ * no longer collides. It is applied to the REGION half too: a region is an S3
77763
+ * key segment (or, for a legacy record, the state body via `readLegacyRegion`),
77764
+ * so it is attacker-chosen in exactly the same way, and a planted region alone
77765
+ * renders `Decoy (x) (us-east-1)` — the same spoof from the right-hand side.
77766
+ *
77767
+ * Every legitimate row is byte-identical, because `displayIdent` is the
77768
+ * identity on a plain identifier and CloudFormation stack names
77769
+ * (`Parent~Child` included) and AWS region codes all are — so no fixture, no
77770
+ * script's grep and no round-trip into `cdkd state show` changes. It keeps
77771
+ * `safe()`'s `UNRENDERABLE` fallback for a value sanitising leaves empty, since
77772
+ * an empty `()` would read as "no region" rather than "a region cdkd will not
77773
+ * print".
77774
+ *
77775
+ * The NAME half passes `STACK_REF_MAX_CODE_POINTS` rather than taking
77776
+ * `displayIdent`'s 255 default, and that is load-bearing for the paragraph
77777
+ * above rather than a tuning knob: a cdkd state-record name is not a
77778
+ * CloudFormation stack name, because `deriveChildStackName` appends
77779
+ * `~<logicalId>` per nesting level, so a legitimate deep nested-stack child
77780
+ * exceeds 255 and would be rendered CUT — a byte change on a legitimate row, in
77781
+ * the middle of a line `while read -r ref` consumes. The REGION half keeps the
77782
+ * default; an AWS region code is at most 25 characters.
77783
+ *
77784
+ * The consequence is taken at ALL SIX callers, the confirmation prompt
77785
+ * included, rather than at the listing alone: guarding one site and leaving
77786
+ * five is the per-site spelling `safe()`'s own comment below records having
77787
+ * failed twice. In the prompt a quoted spoof is the most valuable of the six —
77788
+ * that sentence is the one an operator answers `y` to.
77789
+ *
77790
+ * What the quoting is NOT: shell-safe. A JSON string literal is visually
77791
+ * indistinguishable from a shell DOUBLE-quoted argument, in which `$(...)`,
77792
+ * backticks and `!` still expand — so `"Prod$(touch /tmp/pwn)" (us-east-1)`
77793
+ * must not be pasted into a command line, and this rendering makes no claim
77794
+ * that it may be. The repo's answer for a value that has to survive a command
77795
+ * line is elsewhere and is SUPPRESSION, not quoting: `buildForceUnlockCommand`
77796
+ * declines to print a command at all unless sanitisation was the identity, and
77797
+ * `rollback-executor.ts`'s `PASTEABLE_LOGICAL_ID` records why identity alone is
77798
+ * still not enough (`~user` and `=x` expand). What this function provides is a
77799
+ * visible BOUNDARY for a value a human is reading, which is a different job.
77564
77800
  */
77565
77801
  function formatStackRefSafe(ref) {
77566
- return ref.region ? `${safe$1(ref.stackName)} (${safe$1(ref.region)})` : safe$1(ref.stackName);
77802
+ const name = displayIdent(ref.stackName, { maxCodePoints: STACK_REF_MAX_CODE_POINTS });
77803
+ return ref.region ? `${name} (${displayIdent(ref.region)})` : name;
77567
77804
  }
77568
77805
  /**
77569
77806
  * One spelling of "this value came from an S3 key or a state record, and is
@@ -77588,6 +77825,29 @@ function formatStackRefSafe(ref) {
77588
77825
  * no-op on every legitimate input while an S3 key admits any UTF-8. Free-form
77589
77826
  * text (a parser's own message, an AWS error) takes the denylist class
77590
77827
  * instead; `display-safe.ts`'s header draws that line.
77828
+ *
77829
+ * It is the WEAKER of this file's two spellings and stays so deliberately.
77830
+ * `formatStackRefSafe` above uses `displayIdent` instead, because it renders
77831
+ * cdkd's own ` (region)` annotation right beside the value and the allowlist
77832
+ * cannot stop a value from carrying that annotation itself (issue #3164).
77833
+ * Widening this helper is tracked as go-to-k/cdkd#3179 rather than done here,
77834
+ * for reasons that are per-SITE rather than uniform, so read that issue's
77835
+ * table rather than generalising from this paragraph: most of these sites are
77836
+ * REFUSALS, where the surrounding `'...'` is at least SOME boundary -- but not
77837
+ * all of them are, and ` Region: ${safe(...)}` in the `--long` view below has
77838
+ * no surrounding anything -- while `Run 'cdkd deploy <stack>'` sites are
77839
+ * COMMAND HINTS, where quoting is not this repo's answer at all:
77840
+ * `buildForceUnlockCommand` SUPPRESSES the whole command instead.
77841
+ *
77842
+ * `grep safe(` does NOT enumerate the class, in BOTH directions. Some sites
77843
+ * spell `displaySafe(..., { asciiOnly: true })` inline rather than calling this
77844
+ * helper (`warnOnLiveForeignLock` renders its own `stack (region)` that way,
77845
+ * inside the same `state orphan` output); others sanitise NOTHING
77846
+ * (`stateRefreshObservedCommand` interpolates raw `listStacks` values into its
77847
+ * refusals, including a third `cdkd deploy` hint, and `stateDestroyCommand`
77848
+ * writes raw names into its `--all` confirmation list); and `describeStateKey`
77849
+ * (`state-file-keys.ts`) renders the same shape from raw key segments.
77850
+ * go-to-k/cdkd#3179 enumerates them all; a grep of this helper does not.
77591
77851
  */
77592
77852
  function safe$1(value) {
77593
77853
  return displaySafe(value, { asciiOnly: true }) || "<unrenderable>";
@@ -77841,7 +78101,9 @@ async function stateResourcesCommand(stackName, options) {
77841
78101
  if (!ref.region) throw new Error(`Stack '${safe$1(stackName)}' has only a legacy state record without a region. Run 'cdkd deploy ${safe$1(stackName)}' (or any cdkd write) to migrate it to the region-scoped layout, then re-run this command.`);
77842
78102
  const stateResult = await setup.stateBackend.getState(stackName, ref.region);
77843
78103
  if (!stateResult) throw new Error(`No state found for stack '${safe$1(stackName)}' (${safe$1(ref.region)}) in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`);
77844
- const resources = stateResult.state.resources ?? {};
78104
+ if (repairMalformedResourcesForReadOnly(stateResult.state)) logger.warn(malformedResourcesWarning(stackName, ref.region));
78105
+ if (options.long || options.json) repairRenderedContainers(stateResult.state, RESOURCES_RENDERED_CONTAINERS, stackName, ref.region, logger);
78106
+ const resources = stateResult.state.resources;
77845
78107
  const details = Object.entries(resources).map(([logicalId, entry]) => {
77846
78108
  const resource = entry ?? {};
77847
78109
  return {
@@ -78049,9 +78311,11 @@ async function stateShowCommand(stackName, options) {
78049
78311
  if (options.showNested) {
78050
78312
  const treeWithLocks = await loadLocksForTree(await buildCdkdStateStackTree(stackName, ref.region, setup.stateBackend, stateResult.state), setup.lockManager, lockInfo);
78051
78313
  if (options.json) {
78314
+ warnUnreadableTreeNodes(treeWithLocks, logger);
78052
78315
  process.stdout.write(`${JSON.stringify(treeToShowJson(treeWithLocks), null, 2)}\n`);
78053
78316
  return;
78054
78317
  }
78318
+ repairTreeForTextRender(treeWithLocks, logger);
78055
78319
  const lines = renderTreeWithChildren(treeWithLocks);
78056
78320
  process.stdout.write(`${lines.join("\n")}\n`);
78057
78321
  return;
@@ -78063,6 +78327,7 @@ async function stateShowCommand(stackName, options) {
78063
78327
  }, null, 2)}\n`);
78064
78328
  return;
78065
78329
  }
78330
+ repairRecordForTextRender(stateResult.state, stackName, ref.region, logger);
78066
78331
  process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo, true).join("\n")}\n`);
78067
78332
  } finally {
78068
78333
  setup.dispose();
@@ -78079,6 +78344,11 @@ const SKIPPED_DIGEST_PREVIEW_LEN = 12;
78079
78344
  const UNSERIALIZABLE = "(unserializable)";
78080
78345
  /**
78081
78346
  * The `skippedOutputs` rows a state record owes, sorted by key.
78347
+ *
78348
+ * The `?? {}` covers an ABSENT field — every pre-#2740 record, plus the writers
78349
+ * that drop it. A non-object one is emptied at the render entry instead
78350
+ * (`repairRenderedContainers`, issue go-to-k/cdkd#3187), so this walk and
78351
+ * {@link rendersSkippedBlock}'s cannot disagree about how many rows exist.
78082
78352
  */
78083
78353
  function sortedSkippedOutputs(state) {
78084
78354
  return Object.entries(state.skippedOutputs ?? {}).sort(([a], [b]) => a.localeCompare(b));
@@ -78124,9 +78394,186 @@ function skippedOutputsLegend() {
78124
78394
  ];
78125
78395
  }
78126
78396
  /**
78397
+ * The order {@link repairRenderedContainers} names repaired containers in.
78398
+ *
78399
+ * Fixed here rather than taken from the order they were found in, so two
78400
+ * records that lost the same containers produce the same warning line and a
78401
+ * test can assert the text rather than a set.
78402
+ */
78403
+ const RENDERED_CONTAINER_ORDER = [
78404
+ "outputs",
78405
+ "skippedOutputs",
78406
+ "attributes",
78407
+ "properties"
78408
+ ];
78409
+ /** Every container {@link renderStateBlock}'s text walks. */
78410
+ const SHOW_RENDERED_CONTAINERS = new Set(RENDERED_CONTAINER_ORDER);
78411
+ /**
78412
+ * What `cdkd state resources` walks: the attribute bag alone.
78413
+ *
78414
+ * `properties` is deliberately absent — that command excludes them from every
78415
+ * mode (`docs/cli-state.md` says so and `stateResourcesCommand`'s own doc
78416
+ * repeats it), so warning about a container it never renders would report a
78417
+ * defect the user cannot see in the output in front of them. `outputs` and
78418
+ * `skippedOutputs` are absent for the same reason.
78419
+ */
78420
+ const RESOURCES_RENDERED_CONTAINERS = /* @__PURE__ */ new Set(["attributes"]);
78421
+ /**
78422
+ * Empty every container this view walks but cannot read, and warn ONCE naming
78423
+ * the ones that were emptied (issue go-to-k/cdkd#3187).
78424
+ *
78425
+ * `renderStateBlock` and `stateResourcesCommand --long` walk four more
78426
+ * containers with `Object.entries` than the `resources` bag go-to-k/cdkd#3185
78427
+ * guarded, and `Object.entries` takes a string, a list, a number and a boolean
78428
+ * as readily as an object. Measured on the shipped bundle: `cdkd state show`
78429
+ * over a record whose `outputs` is a 5,000,000-character string and whose
78430
+ * `resources` bag is HEALTHY costs ~1616 ms and ~1010 MB RSS and emits
78431
+ * 5,000,002 lines; the same shape in `skippedOutputs` costs ~2436 ms and
78432
+ * ~2148 MB. `--show-nested` pays that per node.
78433
+ *
78434
+ * AT THE RENDER ENTRY, not at the four loops, which is the rule
78435
+ * `src/state/malformed-resources-bag.ts`'s header records and the measurement
78436
+ * behind it. Here the reason the entry wins is narrower but the same in kind:
78437
+ * the per-resource containers are reached from inside the resource loop, so a
78438
+ * guard written there is one decision repeated per resource per container, and
78439
+ * the `?? {}` already sitting at each of them is exactly the spelling that
78440
+ * looks like a guard and is not.
78441
+ *
78442
+ * **Absent is NOT malformed here, and that is a different call from the one
78443
+ * {@link hasReadableResources} makes.** `undefined` and `null` both mean "no
78444
+ * such container" for all four, and the `?? {}` at each walk has always
78445
+ * rendered the nullish case as empty, so none of them can fabricate a row.
78446
+ * An unreadable `resources` bag is judged the other way because
78447
+ * `Object.entries(null)` THROWS and takes the whole render with it.
78448
+ *
78449
+ * The stronger half of the reason covers TWO of the four, not all of them, and
78450
+ * saying so is the point: `skippedOutputs?` and `attributes?` are OPTIONAL in
78451
+ * `src/types/state.ts`, so cdkd itself writes records without them and warning
78452
+ * would be a false positive on healthy state. `outputs` and `properties` are
78453
+ * REQUIRED there, so a nullish one IS a malformed record and this exemption
78454
+ * rests on the weaker reason alone — it cannot invent a row, and it renders
78455
+ * identically to the empty bag the reader would otherwise see. Warning on a
78456
+ * nullish REQUIRED container would be defensible; it is not done because the
78457
+ * harm this guard exists for is fabrication (review of go-to-k/cdkd#3190).
78458
+ *
78459
+ * Callers must have repaired the `resources` bag first; both do — one line up
78460
+ * in `repairRecordForTextRender`, and ~30 lines up in `stateResourcesCommand`,
78461
+ * where the mode gate sits between them.
78462
+ * The bag test below is that ORDER made local rather than a fallback: an
78463
+ * unreadable bag is emptied by the repair, so the record renders zero resources
78464
+ * and owns no per-resource container to repair. It reads
78465
+ * {@link isReadableBag} rather than {@link hasReadableResources} on purpose —
78466
+ * the latter is the probe `tests/unit/cli/state-record-shape.test.ts` counts to
78467
+ * measure the nested walk, and a second caller here would silently inflate it.
78468
+ */
78469
+ function repairRenderedContainers(state, walked, stackName, region, logger) {
78470
+ const repaired = /* @__PURE__ */ new Set();
78471
+ const emptyIfUnwalkable = (owner, name) => {
78472
+ if (!walked.has(name)) return;
78473
+ const container = owner[name];
78474
+ if (container === void 0 || container === null) return;
78475
+ if (isReadableBag(container)) return;
78476
+ owner[name] = {};
78477
+ repaired.add(name);
78478
+ };
78479
+ const record = state;
78480
+ emptyIfUnwalkable(record, "outputs");
78481
+ emptyIfUnwalkable(record, "skippedOutputs");
78482
+ if ((walked.has("attributes") || walked.has("properties")) && isReadableBag(state.resources)) for (const entry of Object.values(state.resources)) {
78483
+ if (!isReadableBag(entry)) continue;
78484
+ const fields = entry;
78485
+ emptyIfUnwalkable(fields, "attributes");
78486
+ emptyIfUnwalkable(fields, "properties");
78487
+ }
78488
+ if (repaired.size === 0) return;
78489
+ logger.warn(malformedRenderedContainersWarning(stackName, region, RENDERED_CONTAINER_ORDER.filter((name) => repaired.has(name))));
78490
+ }
78491
+ /**
78492
+ * Give the TEXT render a readable `resources` bag and readable value
78493
+ * containers, and warn when either was a repair rather than a no-op.
78494
+ *
78495
+ * {@link renderStateBlock} walks the bag with `Object.entries`, which takes a
78496
+ * string, a list, a number and a boolean as readily as an object. A planted
78497
+ * `"abcdef"` rendered six `[index, character]` pairs as six resources that do
78498
+ * not exist, under a `Resources (6):` header — and the cost grows with the
78499
+ * planted length, since one pair is allocated per character.
78500
+ *
78501
+ * Called AFTER every `--json` branch rather than at the load, which is this
78502
+ * command's one departure from the rule `src/state/malformed-resources-bag.ts`
78503
+ * states, and is deliberate: `cdkd state show --json` is the command
78504
+ * `cdkd state list --long` names as the way to SEE a record it could not count,
78505
+ * so repairing above that branch would hand the operator a well-formed
78506
+ * `"resources": {}` and delete the evidence they came for.
78507
+ *
78508
+ * That placement is only safe because ONE thing reads the bag earlier — the
78509
+ * `--show-nested` walker — and `walkCdkdStateStackTree` now returns a childless
78510
+ * node on an unreadable bag before it dereferences one. The comment at that
78511
+ * guard carries what the walker did with such a bag before, in both directions:
78512
+ * a scalar-element bag yielded no children but still allocated per element, and
78513
+ * a LIST of resource objects hard-failed. Do not move this call up, and do not
78514
+ * drop that guard: either one alone leaves the other half live.
78515
+ *
78516
+ * The guard is in the WALKER and not at its caller because the walker recurses.
78517
+ * A caller-side test covers the root record only, which is what an earlier cut
78518
+ * of this fix shipped — a healthy root naming a nested child still walked that
78519
+ * child's bag unguarded.
78520
+ *
78521
+ * The container pass runs SECOND and under the same `--json` placement, for one
78522
+ * reason each. Second, because {@link repairRenderedContainers} reads the
78523
+ * resource entries and the repair above is what guarantees there is a map to
78524
+ * read them from. Under the same placement, because `--json` emits the record
78525
+ * as parsed and a non-object `outputs` is evidence there exactly as a
78526
+ * non-object `resources` bag is — and because the payload carries the stored
78527
+ * value whole, it fabricates nothing to emit it.
78528
+ */
78529
+ function repairRecordForTextRender(state, stackName, region, logger) {
78530
+ if (repairMalformedResourcesForReadOnly(state)) logger.warn(malformedResourcesWarning(stackName, region));
78531
+ repairRenderedContainers(state, SHOW_RENDERED_CONTAINERS, stackName, region, logger);
78532
+ }
78533
+ /**
78534
+ * The same repair for every node {@link renderTreeWithChildren} will render,
78535
+ * the root included. Each node carries its own record, so a CHILD with a
78536
+ * malformed bag fabricates rows exactly as the root does; one warning per
78537
+ * repaired record names which stack it came from.
78538
+ *
78539
+ * Both identifiers are strings by construction rather than record fields: the
78540
+ * walker builds a child's name as `` `${parent}~${logicalId}` `` and threads the
78541
+ * region it walked against, having already refused any child whose recorded
78542
+ * region disagrees.
78543
+ */
78544
+ function repairTreeForTextRender(node, logger) {
78545
+ repairRecordForTextRender(node.state, node.stackName, node.region, logger);
78546
+ for (const child of node.children) repairTreeForTextRender(child, logger);
78547
+ }
78548
+ /**
78549
+ * Say which nodes the walk could not read, WITHOUT repairing any of them.
78550
+ *
78551
+ * `--show-nested --json` is the one mode that must not repair — it exists to
78552
+ * show the operator the stored record — but "cannot repair" is not "must stay
78553
+ * silent". An unreadable bag makes the walk return that node childless, and a
78554
+ * childless node is byte-indistinguishable from a genuine leaf: a consumer
78555
+ * enumerating `children` reads a CUT subtree as a complete one. The text views
78556
+ * do not have this problem, because a cut node renders `Resources (0):` where
78557
+ * the operator expected rows.
78558
+ *
78559
+ * `logger.warn` writes to stderr, and `stateShowCommand` has already called
78560
+ * `reserveStdoutForPayload()` for this branch, so this cannot corrupt the JSON.
78561
+ */
78562
+ function warnUnreadableTreeNodes(node, logger) {
78563
+ if (!hasReadableResources(node.state)) logger.warn(malformedResourcesWarning(node.stackName, node.region));
78564
+ for (const child of node.children) warnUnreadableTreeNodes(child, logger);
78565
+ }
78566
+ /**
78127
78567
  * Render one stack's state record as a human-readable multi-line block —
78128
78568
  * the shared body used by both the single-stack default output and each
78129
78569
  * nested child rendered under `--show-nested`.
78570
+ *
78571
+ * Every caller must have run {@link repairRecordForTextRender} over the
78572
+ * record first (`repairTreeForTextRender` does it per node for the tree): the
78573
+ * resource walk below is what fabricates rows from a non-object bag, and the
78574
+ * `state.ts names renderStateBlock in CODE exactly 1 + 3 times` case in
78575
+ * `tests/unit/cli/state-record-shape.test.ts` refuses a fourth call site so
78576
+ * that choice cannot be made silently.
78130
78577
  */
78131
78578
  function renderStateBlock(state, lockInfo, withLegend) {
78132
78579
  const lines = [];
@@ -78157,7 +78604,7 @@ function renderStateBlock(state, lockInfo, withLegend) {
78157
78604
  }
78158
78605
  if (withLegend) lines.push(...skippedOutputsLegend());
78159
78606
  }
78160
- const resourceEntries = Object.entries(state.resources ?? {}).map(([logicalId, entry]) => [logicalId, entry ?? {}]).sort(([a], [b]) => a.localeCompare(b));
78607
+ const resourceEntries = Object.entries(state.resources).map(([logicalId, entry]) => [logicalId, entry ?? {}]).sort(([a], [b]) => a.localeCompare(b));
78161
78608
  lines.push("");
78162
78609
  lines.push(`Resources (${resourceEntries.length}):`);
78163
78610
  for (const [logicalId, resource] of resourceEntries) {
@@ -79238,11 +79685,39 @@ function snapshotNote(resourceType, effectiveProvisionedBy, skipFinalSnapshot) {
79238
79685
  * one calls `displaySafe()` directly and takes the DENYLIST -- the same class
79239
79686
  * `formatError` picks for a `cause`, and for the same reason. `grep displaySafe(`
79240
79687
  * answers how many; a count written here was wrong on its first revision.
79688
+ *
79689
+ * Scope is answered by grepping BOTH `safe(` and `safeStack(` -- the latter is
79690
+ * not matched by the former, and a sentence naming only one understates the
79691
+ * population by twelve.
79241
79692
  */
79242
79693
  function safe(value) {
79243
79694
  return displayIdent(value);
79244
79695
  }
79245
79696
  /**
79697
+ * `safe()` for a cdkd STATE-RECORD STACK NAME specifically, which is the one
79698
+ * value class in this file whose legitimate grammar runs past `displayIdent`'s
79699
+ * 255-code-point default (issue #3164).
79700
+ *
79701
+ * A stack name here is not a CloudFormation stack name: `deriveChildStackName`
79702
+ * appends `~<logicalId>` per nesting level, and with CDK's ~60-character
79703
+ * generated nested-stack logical ids a legitimate child passes 255 around the
79704
+ * fourth level. Cutting one is a byte change on a LEGITIMATE value, and this
79705
+ * file is the worst place for it -- three of its renders are
79706
+ * `re-run 'cdkd rollback <stack>'` COPY-PASTE hints and one is the
79707
+ * confirmation prompt, so a cut name hands an operator an unrunnable command
79708
+ * mid-incident.
79709
+ *
79710
+ * It is a NAMED helper rather than a `maxCodePoints` argument repeated per
79711
+ * site, because a per-site spelling of exactly this rule is what issue #3164
79712
+ * exists to stop: the first cut of that fix widened ONE of this file's twelve
79713
+ * stack-name renders and left eleven cut. Every value that is NOT a stack name --
79714
+ * a region (at most 25 characters), a logical id, a resource type, a change
79715
+ * type -- keeps `safe()` and its tighter default.
79716
+ */
79717
+ function safeStack(value) {
79718
+ return displayIdent(value, { maxCodePoints: STACK_REF_MAX_CODE_POINTS });
79719
+ }
79720
+ /**
79246
79721
  * Human label for a planned rollback action (plan preview). `skipFinalSnapshot`
79247
79722
  * is threaded in because the classifier is pure (it cannot see CLI flags) and
79248
79723
  * the Snapshot label would otherwise promise a final snapshot the run is about
@@ -79321,7 +79796,7 @@ async function rollbackCommand(stackArg, options) {
79321
79796
  return;
79322
79797
  }
79323
79798
  if (scoped.length > 1) {
79324
- const list = scoped.map((c) => ` - ${safe(c.stackName)} (${safe(c.region)})`).join("\n");
79799
+ const list = scoped.map((c) => ` - ${safeStack(c.stackName)} (${safe(c.region)})`).join("\n");
79325
79800
  throw new Error(`Multiple stacks have a rollback journal. Pick one:\n${list}\nRe-run 'cdkd rollback <stack>' (add --stack-region if the same name spans regions).`);
79326
79801
  }
79327
79802
  ref = scoped[0];
@@ -79354,8 +79829,8 @@ async function rollbackCommand(stackArg, options) {
79354
79829
  try {
79355
79830
  const stateData = await setup.stateBackend.getState(stackName, region);
79356
79831
  const journal = await setup.stateBackend.loadRollbackJournal(stackName, region);
79357
- if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${safe(stackName)}' (${safe(region)}). Run 'cdkd deploy' to (re)deploy, or 'cdkd destroy' to clean up.`);
79358
- if (!stateData) throw new Error(`Rollback journal exists for '${safe(stackName)}' (${safe(region)}) but its state.json is missing (keys: ${safe(`${setup.prefix}/${stackName}/${region}`)}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
79832
+ if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${safeStack(stackName)}' (${safe(region)}). Run 'cdkd deploy' to (re)deploy, or 'cdkd destroy' to clean up.`);
79833
+ if (!stateData) throw new Error(`Rollback journal exists for '${safeStack(stackName)}' (${safe(region)}) but its state.json is missing (keys: ${safe(setup.prefix)}/${safeStack(stackName)}/${safe(region)}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
79359
79834
  const baseState = stateData.state;
79360
79835
  refuseMalformedState(baseState, stackName, region);
79361
79836
  const stateResources = { ...baseState.resources };
@@ -79363,7 +79838,7 @@ async function rollbackCommand(stackArg, options) {
79363
79838
  const orphanLogicalIds = new Set(options.orphan ?? []);
79364
79839
  const newestSegment = journal.segments[journal.segments.length - 1];
79365
79840
  if (newestSegment.roleArn && !options.roleArn) logger.info(`Note: the failed deploy ran with --role-arn ${safe(newestSegment.roleArn)}; this rollback is running with ambient credentials (pass --role-arn to match).`);
79366
- logger.info(`\nRollback plan for '${safe(stackName)}' (${safe(region)}):`);
79841
+ logger.info(`\nRollback plan for '${safeStack(stackName)}' (${safe(region)}):`);
79367
79842
  const planStateView = { ...stateResources };
79368
79843
  for (let s = journal.segments.length - 1; s >= 0; s--) {
79369
79844
  const segment = journal.segments[s];
@@ -79381,7 +79856,7 @@ async function rollbackCommand(stackArg, options) {
79381
79856
  }
79382
79857
  logger.info("");
79383
79858
  if (!skipConfirmation) {
79384
- if (!await confirm(`Roll back '${safe(stackName)}' (${safe(region)})?`)) {
79859
+ if (!await confirm(`Roll back '${safeStack(stackName)}' (${safe(region)})?`)) {
79385
79860
  logger.info("Rollback cancelled");
79386
79861
  return;
79387
79862
  }
@@ -79419,7 +79894,7 @@ async function rollbackCommand(stackArg, options) {
79419
79894
  const fresh = await setup.stateBackend.getState(stackName, region);
79420
79895
  currentEtag = await setup.stateBackend.saveState(stackName, region, next(), { ...fresh?.etag !== void 0 && { expectedEtag: fresh.etag } });
79421
79896
  } catch (retryError) {
79422
- logger.warn(`Failed to persist state after a rollback operation: ${displaySafe(retryError instanceof Error ? retryError.message : String(retryError))}. The resource was reverted in AWS; re-run 'cdkd rollback ${safe(stackName)}' to reconcile state.`);
79897
+ logger.warn(`Failed to persist state after a rollback operation: ${displaySafe(retryError instanceof Error ? retryError.message : String(retryError))}. The resource was reverted in AWS; re-run 'cdkd rollback ${safeStack(stackName)}' to reconcile state.`);
79423
79898
  }
79424
79899
  }
79425
79900
  };
@@ -79498,16 +79973,16 @@ async function rollbackCommand(stackArg, options) {
79498
79973
  const survivingOrphans = orphansAfterRollback(baseState, mintedOrphans).orphans ?? [];
79499
79974
  if (journal.segments.length === 0 && oldestInitialDeploy && Object.keys(stateResources).length === 0 && survivingOrphans.length === 0) {
79500
79975
  await setup.stateBackend.deleteState(stackName, region);
79501
- logger.info(`State for '${safe(stackName)}' (${safe(region)}) removed (stack fully rolled back).`);
79976
+ logger.info(`State for '${safeStack(stackName)}' (${safe(region)}) removed (stack fully rolled back).`);
79502
79977
  }
79503
- if (interrupted) throw new PartialFailureError(`Rollback interrupted. Journal preserved — re-run 'cdkd rollback ${safe(stackName)}' to finish.`);
79504
- if (totalFailures > 0) throw new PartialFailureError(`Rollback completed with ${totalFailures} failed operation(s). Journal preserved — re-run 'cdkd rollback ${safe(stackName)}' to retry.`);
79978
+ if (interrupted) throw new PartialFailureError(`Rollback interrupted. Journal preserved — re-run 'cdkd rollback ${safeStack(stackName)}' to finish.`);
79979
+ if (totalFailures > 0) throw new PartialFailureError(`Rollback completed with ${totalFailures} failed operation(s). Journal preserved — re-run 'cdkd rollback ${safeStack(stackName)}' to retry.`);
79505
79980
  if (totalWarnings > 0) throw new PartialFailureError(`Rollback completed with ${totalWarnings} skipped/unrecoverable operation(s) (see warnings above).`);
79506
- logger.info(`\nRollback of '${safe(stackName)}' (${safe(region)}) complete.`);
79981
+ logger.info(`\nRollback of '${safeStack(stackName)}' (${safe(region)}) complete.`);
79507
79982
  } finally {
79508
79983
  try {
79509
79984
  await setup.lockManager.releaseLock(stackName, region).catch((err) => {
79510
- logger.warn(`Failed to release lock for '${safe(stackName)}' (${safe(region)}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
79985
+ logger.warn(`Failed to release lock for '${safeStack(stackName)}' (${safe(region)}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
79511
79986
  });
79512
79987
  } finally {
79513
79988
  process.removeListener("SIGINT", sigintHandler);
@@ -92998,4 +93473,4 @@ function buildProgram() {
92998
93473
 
92999
93474
  //#endregion
93000
93475
  export { buildProgram };
93001
- //# sourceMappingURL=program-Ckl60p0E.js.map
93476
+ //# sourceMappingURL=program-C7UAuWDp.js.map