@go-to-k/cdkd 0.285.7 → 0.285.9

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 DEFAULT_STATE_PREFIX, $n as CFN_TEMPLATE_BODY_LIMIT, $t as maskSecretsInError, A as bold, An as describeAwsFailure, Ar as NestedStackChildDirectDestroyError, At as requireConfigObject, B as secretBearingStateKeyWarning, Bn as Synthesizer, Bt as DiffCalculator, C as unsupportedFinalSnapshotError, Cn as getBootstrapMarkerKey, Cr as DeployCancelledError, Ct as assertRegionMatch, D as isStatefulRecreateTargetSync, Dn as validateAssetBucketName, Dr as LocalStartServiceError, Dt as readConfigString, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as readBootstrapMarkerBody, Er as LocalMigrateError, Et as configStringRefusal, F as yellow, Fn as partitionSensitiveEnv, Fr as StackHasActiveImportsError, Ft as s3BucketDomainName, G as clearOnUpdateRemoval, Gn as resolveAutoAssetStorage, Gr as isThrottlingError, Gt as TemplateParser, H as getCurrentResourceSecrets, Hn as getDefaultStateBucketName, Hr as withErrorHandling, Ht as describeTypeWithThrottleRetry, I as collectDeclaredOutputNames, In as runDockerForeground, Ir as StackTerminationProtectionError, It as s3BucketDualStackDomainName, J as findSilentDropProperties, Jn as resolveStateBucketWithDefault, Jr as markRedactedCause, Jt as TEMPLATE_SOURCED_RULES, K as ProviderRegistry, Kn as resolveCaptureObservedState, Kr as isTransientServerError, Kt as STATE_SOURCED_CROSS_GENERATION_RULES, L as collectPublishedOutputNames, Ln as runDockerStreaming, Lt as s3BucketRegionalDomainName, M as gray, Mn as dockerSpawnEnvWithSensitive, Mr as ProvisioningError, Mt as classifyReplaySecretRegion, N as green, Nn as formatDockerLoginError, Nr as ResourceTimeoutError, Nt as producerRegionsFromState, O as renderStatefulReason, On as validateContainerRepoName, Ot as replayWarn, P as red, Pn as getDockerCmd, Pr as ResourceUpdateNotSupportedError, Pt as s3BucketArn, Q as CUSTOM_RESOURCE_RESPONSE_PREFIX, Qn as warnDeprecatedNoPrefixCliFlag, Qt as isSingleDynamicReferenceToken, R as exportAliasCollisionScrubWarning, Rn as AssetManifestLoader, Rt as s3BucketWebsiteUrl, S as refusesFinalSnapshot, Sn as ensureAssetStorage, St as resolveExplicitPhysicalId, T as extractDeploymentEventError, Tn as parseBootstrapMarker, Tr as LocalInvokeBuildError$1, Tt as configBooleanRefusal, U as IAMRoleProvider, Un as getLegacyStateBucketName, Ur as isMarkedNonRetryable, Ut as withRetry, V as stateKeySecretExposure, Vn as synthesisStatusMessage, Vr as normalizeAwsError, Vt as INTRINSIC_KEYS, W as collectInlinePolicyNamesManagedBySiblings, Wn as resolveApp, Wr as isRetryableTransientError, Wt as DagBuilder, X as maskDeep, Xn as resolveUseCdkBootstrapAssets, Xt as dynamicReferenceTokens, Y as createMaskedRetryLogger, Yn as resolveStateBucketWithDefaultAndSource, Yr as retryClassificationText, Yt as createSecretMasker, Z as maskerOrIdentity, Zn as stateBucketExistenceConfirmed, Zt as errorCauseChain, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as escapeRegExp, _r as setAwsClients, _t as isUnboundTemplateParameter, a as DeploymentEventsStore, an as rebuildClientForBucketRegion, ar as displaySafe, at as CloudControlProvider, b as createPreDeleteFinalSnapshot, bn as BOOTSTRAP_MARKER_PREFIX, bt as WAFv2WebACLProvider, c as replayFailedOperations, cn as importableOutputs, cr as canonicalizeRegion, ct as deleteIndeterminateGuards, d as updatePartialReason, dn as stringifyValue, dr as processStackMessages, dt as isTerminationProtectionPropagationError, en as maskSecretsInText, er as CFN_TEMPLATE_URL_LIMIT, et as beginCommandInterruptScope, f as withResourceDeadline, fn as WorkGraph, ft as IntrinsicFunctionResolver, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as rewriteTemplateAssetReferences, gr as resetAwsClients, gt as getAccountInfo, h as computeImplicitDeleteEdges, hn as loadPublishableAssetManifest, hr as getAwsClients, ht as coerceParameterTypedValue, i as DeploymentEventsReader, in as S3StateBackend, ir as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, it as startInterruptWatch, j as cyan, jn as buildDockerImage, jr as PartialFailureError, jt as requireConfigString, k as formatResourceLine, kn as buildDenyExternalAccessPolicy, kr as MissingCdkCliError, kt as requireConfigArray, l as replayRollback, ln as shouldRetainResource, lr as derivePartitionAndUrlSuffix, lt as deleteSkipReason, m as IMPLICIT_DELETE_DEPENDENCIES, mn as createAssetRedirectResolver, mr as AwsClients, mt as cfnRefValueFromPhysicalId, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as scrubResourceRecord, nr as findLargeInlineResources, nt as interruptWatchListenerCount, o as planFailedOps, on as exportNamesCarriedFrom, or as expectedOwnerParam, ot as slowCcOperationTimeoutMs, p as maskingRetryLogger, pn as buildAssetRedirectMap, pr as resolveBucketRegion, pt as carriesDynamicReference, q as findActionableSilentDrops, qn as resolveSkipPrefix, qr as markNonRetryable, qt as STATE_SOURCED_READBACK_RULES, r as DeployEngine, rn as LockManager, rr as uploadCfnTemplate, rt as isInterruptedWaitError, s as planRollback, sn as importableOutputKeys, sr as PARTITION_TABLE, st as UNSPECIFIED_SKIP_REASON, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as redactSecretsForState, tr as MIGRATE_TMP_PREFIX, tt as endCommandInterruptScope, u as updatePartialMessage, un as AssetPublisher, ur as AssemblyReader, ut as disableInstanceApiTermination, v as buildFinalSnapshotIdentifier, vn as stripControlChars, vt as parameterTypeMayLoseSecretIdentity, w as makeCanonicalizePropertiesFn, wn as isCrossRegionRedirect, wr as DynamicReferenceRegionAmbiguousError, wt as coerceCfnBoolean, x as isFinalSnapshotError, xn as assertAssetBucketRegion, xr as CrossAccountSecretRefusalError, xt as normalizeAwsTagsToCfn, y as ccRoutedFinalSnapshotError, yn as AssetModeResolver, yr as CdkdError, yt as refStateLookupFromResource, z as isExportAliasCollision, zn as getDockerImageBySourceHash, zt as applyRoleArnIfSet } from "./deploy-engine-DmWPwiyQ.js";
2
- import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-DyTk5GeO.js";
1
+ import { $ as DEFAULT_STATE_PREFIX, $n as resolveUseCdkBootstrapAssets, $t as isSingleDynamicReferenceToken, A as bold, An as validateAssetBucketName, Ar as LocalStartServiceError, At as requireConfigObject, B as secretBearingStateKeyWarning, Bn as runDockerStreaming, Bt as DiffCalculator, C as unsupportedFinalSnapshotError, Cn as BOOTSTRAP_MARKER_PREFIX, Ct as assertRegionMatch, D as isStatefulRecreateTargetSync, Dn as isCrossRegionRedirect, Dr as DynamicReferenceRegionAmbiguousError, Dt as readConfigString, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as getBootstrapMarkerKey, Er as DeployCancelledError, Et as configStringRefusal, F as yellow, Fn as dockerSpawnEnvWithSensitive, Fr as ProvisioningError, Ft as s3BucketDomainName, G as clearOnUpdateRemoval, Gn as getDefaultStateBucketName, Gr as withErrorHandling, Gt as TemplateParser, H as getCurrentResourceSecrets, Hn as getDockerImageBySourceHash, Ht as describeTypeWithThrottleRetry, I as collectDeclaredOutputNames, In as formatDockerLoginError, Ir as ResourceTimeoutError, It as s3BucketDualStackDomainName, J as findSilentDropProperties, Jn as resolveAutoAssetStorage, Jr as isThrottlingError, Jt as TEMPLATE_SOURCED_RULES, K as ProviderRegistry, Kn as getLegacyStateBucketName, Kr as isMarkedNonRetryable, Kt as STATE_SOURCED_CROSS_GENERATION_RULES, L as collectPublishedOutputNames, Ln as getDockerCmd, Lr as ResourceUpdateNotSupportedError, Lt as s3BucketRegionalDomainName, M as gray, Mn as buildDenyExternalAccessPolicy, Mr as MissingCdkCliError, Mt as classifyReplaySecretRegion, N as green, Nn as describeAwsFailure, Nr as NestedStackChildDirectDestroyError, Nt as producerRegionsFromState, O as renderStatefulReason, On as parseBootstrapMarker, Or as LocalInvokeBuildError$1, Ot as replayWarn, P as red, Pn as buildDockerImage, Pr as PartialFailureError, Pt as s3BucketArn, Q as CUSTOM_RESOURCE_RESPONSE_PREFIX, Qn as resolveStateBucketWithDefaultAndSource, Qr as retryClassificationText, Qt as errorCauseChain, R as exportAliasCollisionScrubWarning, Rn as partitionSensitiveEnv, Rr as StackHasActiveImportsError, Rt as s3BucketWebsiteUrl, S as refusesFinalSnapshot, Sn as AssetModeResolver, Sr as CdkdError, St as resolveExplicitPhysicalId, T as extractDeploymentEventError, Tn as ensureAssetStorage, Tt as configBooleanRefusal, U as IAMRoleProvider, Un as Synthesizer, Ut as withRetry, V as stateKeySecretExposure, Vn as AssetManifestLoader, Vt as INTRINSIC_KEYS, W as collectInlinePolicyNamesManagedBySiblings, Wn as synthesisStatusMessage, Wr as normalizeAwsError, Wt as DagBuilder, X as maskDeep, Xn as resolveSkipPrefix, Xr as markNonRetryable, Xt as createSecretMasker, Y as createMaskedRetryLogger, Yn as resolveCaptureObservedState, Yr as isTransientServerError, Yt as carriesSecretMask, Z as maskerOrIdentity, Zn as resolveStateBucketWithDefault, Zr as markRedactedCause, Zt as dynamicReferenceTokens, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as createAssetRedirectResolver, _r as AwsClients, _t as isUnboundTemplateParameter, a as DeploymentEventsStore, an as scrubResourceRecord, ar as findLargeInlineResources, at as CloudControlProvider, b as createPreDeleteFinalSnapshot, bn as escapeRegExp, br as setAwsClients, bt as WAFv2WebACLProvider, c as replayFailedOperations, cn as rebuildClientForBucketRegion, cr as displaySafe, ct as deleteIndeterminateGuards, d as updatePartialReason, dn as importableOutputs, dr as canonicalizeRegion, dt as isTerminationProtectionPropagationError, en as maskSecretsInError, er as stateBucketExistenceConfirmed, et as beginCommandInterruptScope, f as withResourceDeadline, fn as shouldRetainResource, fr as derivePartitionAndUrlSuffix, ft as IntrinsicFunctionResolver, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as buildAssetRedirectMap, gr as resolveBucketRegion, gt as getAccountInfo, h as computeImplicitDeleteEdges, hn as WorkGraph, ht as coerceParameterTypedValue, i as DeploymentEventsReader, in as redactSecretsForState, ir as MIGRATE_TMP_PREFIX, it as startInterruptWatch, j as cyan, jn as validateContainerRepoName, jt as requireConfigString, k as formatResourceLine, kn as readBootstrapMarkerBody, kr as LocalMigrateError, kt as requireConfigArray, l as replayRollback, ln as exportNamesCarriedFrom, lr as expectedOwnerParam, lt as deleteSkipReason, m as IMPLICIT_DELETE_DEPENDENCIES, mn as stringifyValue, mr as processStackMessages, mt as cfnRefValueFromPhysicalId, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as recordMaskOnlyValue, nr as CFN_TEMPLATE_BODY_LIMIT, nt as interruptWatchListenerCount, o as planFailedOps, on as LockManager, or as uploadCfnTemplate, ot as slowCcOperationTimeoutMs, p as maskingRetryLogger, pn as AssetPublisher, pr as AssemblyReader, pt as carriesDynamicReference, q as findActionableSilentDrops, qn as resolveApp, qr as isRetryableTransientError, qt as STATE_SOURCED_READBACK_RULES, r as DeployEngine, rn as recoverMaskedOutput, rr as CFN_TEMPLATE_URL_LIMIT, rt as isInterruptedWaitError, s as planRollback, sn as S3StateBackend, sr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, st as UNSPECIFIED_SKIP_REASON, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as maskSecretsInText, tr as warnDeprecatedNoPrefixCliFlag, tt as endCommandInterruptScope, u as updatePartialMessage, un as importableOutputKeys, ur as PARTITION_TABLE, ut as disableInstanceApiTermination, v as buildFinalSnapshotIdentifier, vn as loadPublishableAssetManifest, vr as getAwsClients, vt as parameterTypeMayLoseSecretIdentity, w as makeCanonicalizePropertiesFn, wn as assertAssetBucketRegion, wr as CrossAccountSecretRefusalError, wt as coerceCfnBoolean, x as isFinalSnapshotError, xn as stripControlChars, xt as normalizeAwsTagsToCfn, y as ccRoutedFinalSnapshotError, yn as rewriteTemplateAssetReferences, yr as resetAwsClients, yt as refStateLookupFromResource, z as isExportAliasCollision, zn as runDockerForeground, zr as StackTerminationProtectionError, zt as applyRoleArnIfSet } from "./deploy-engine-BhBR2d3w.js";
2
+ import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-DlUEyyGa.js";
5
- import { t as ASGProvider } from "./asg-provider-B1EEjygi.js";
4
+ import { t as getCdkdVersion } from "./version-BWsyP2sq.js";
5
+ import { t as ASGProvider } from "./asg-provider-hNkWDuAJ.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, 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";
@@ -2675,14 +2675,32 @@ async function promptRecreateConfirm(input) {
2675
2675
  //#endregion
2676
2676
  //#region src/cli/commands/confirm-prompt.ts
2677
2677
  /**
2678
- * Default-YES confirmation prompt (`[Y/n]`, empty input = yes). Used by the
2679
- * issue #1007 asset-storage auto-create on interactive TTY deploys without
2680
- * `--yes`. Kept in its own module (mirrors `recreate-confirm-prompt.ts`) so
2681
- * the accept/decline parsing is unit-testable without importing the whole
2682
- * deploy command.
2678
+ * The CLI's two shared confirmation-prompt helpers.
2679
+ *
2680
+ * They have OPPOSITE defaults and opposite non-interactive behaviour, which is
2681
+ * why both live here rather than one being expressed in terms of the other:
2682
+ *
2683
+ * - {@link promptYesNo} — default-YES (`[Y/n]`, empty input accepts). Used by
2684
+ * the issue #1007 asset-storage auto-create on interactive TTY deploys
2685
+ * without `--yes`. It deliberately carries NO non-interactive guard: its
2686
+ * only caller (`deploy.ts`, at the `options.yes || !process.stdin.isTTY`
2687
+ * short-circuit) never reaches it on a non-TTY stdin, and a deploy that
2688
+ * assumes "yes" is the one place in the CLI where auto-confirming is the
2689
+ * documented choice.
2690
+ * - {@link confirmOrRefuse} — default-NO (`[y/N]`), and REFUSES outright on a
2691
+ * non-interactive stdin. This is the one every MUTATING command uses.
2692
+ *
2693
+ * Kept in its own module (mirrors `recreate-confirm-prompt.ts`) so the
2694
+ * accept/decline parsing is unit-testable without importing whole command
2695
+ * files.
2696
+ */
2697
+ /**
2698
+ * Default-YES confirmation prompt (`[Y/n]`, empty input = yes).
2699
+ *
2700
+ * See the module doc for why this one is deliberately unguarded.
2683
2701
  */
2684
2702
  async function promptYesNo(prompt) {
2685
- const rl = readline.createInterface({
2703
+ const rl = readline$1.createInterface({
2686
2704
  input: process.stdin,
2687
2705
  output: process.stdout
2688
2706
  });
@@ -2693,6 +2711,79 @@ async function promptYesNo(prompt) {
2693
2711
  rl.close();
2694
2712
  }
2695
2713
  }
2714
+ /** Suffix appended to a {@link confirmOrRefuse} prompt when none is given. */
2715
+ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2716
+ /**
2717
+ * Default-NO confirmation prompt that REFUSES a non-interactive run instead of
2718
+ * hanging on it (issue #2275, the class issue #2259 fixed for `cdkd destroy`).
2719
+ *
2720
+ * `rl.question` never settles when stdin is already at EOF, and EOF delivers
2721
+ * no signal, so nothing wakes it: a command that prompts without this guard
2722
+ * parks FOREVER in CI rather than failing, burning the job's whole timeout
2723
+ * budget. Measured on Node 24.15.0, the version `.node-version` pins, against
2724
+ * real `node:readline/promises`: `echo y |` resolves `"y"`, while both
2725
+ * `printf 'y' |` (a real answer with no trailing newline) and `< /dev/null`
2726
+ * stay pending indefinitely.
2727
+ *
2728
+ * REFUSE rather than auto-confirm, at every one of the nine sites this helper
2729
+ * replaced. Every one of them guards a MUTATION — a rollback replay, a state
2730
+ * record removal, an observed-property refresh, an orphan, an import, an
2731
+ * export-then-delete-state, a drift accept/revert, a CloudFormation stack
2732
+ * retirement, a state-bucket migration — so silently answering "yes" on
2733
+ * behalf of an absent operator is never the safe reading. `deploy.ts`'s
2734
+ * auto-confirm (`options.yes || !process.stdin.isTTY` -> proceed) stays the
2735
+ * deliberate exception, because a deploy that assumes "yes" is recoverable.
2736
+ *
2737
+ * A first cut of this guard (in `state.ts`, see the long comment at the
2738
+ * `state destroy --all` prompt) raced the question against readline's `close`
2739
+ * event and turned EOF into a decline. That lost three measured ways — a real
2740
+ * answer with no trailing newline was silently discarded, a delayed answer
2741
+ * lost the race, and an interactive Ctrl-C landed on the EOF arm — so do not
2742
+ * reintroduce it. A refusal has none of those.
2743
+ *
2744
+ * POSITION IS LOAD-BEARING, and this helper gets it for free: every call site
2745
+ * sits INSIDE the caller's `--yes` / `--force` short-circuit
2746
+ * (`if (!options.yes && !options.force) { ... }`), so a flagged
2747
+ * non-interactive run never reaches this function and never consults stdin at
2748
+ * all. Keep it that way — hoisting a call above its flag check would make
2749
+ * `--yes` refuse instead of proceed, which is the inverse bug.
2750
+ *
2751
+ * Throws {@link CdkdError} with the `NON_INTERACTIVE_CONFIRM` code, matching
2752
+ * `gc.ts` and `bootstrap-destroy.ts` — the only two of the five originally
2753
+ * guarded prompts that carry a code (the other three throw a bare `Error` or
2754
+ * a `LocalMigrateError`). `handleError` maps it to exit 1, so CI can branch on
2755
+ * it.
2756
+ *
2757
+ * TWO GUARDED PROMPTS DELIBERATELY DO NOT ROUTE THROUGH HERE:
2758
+ * `destroy-runner.ts`'s per-stack prompt (the issue #2259 fix) and
2759
+ * `state.ts`'s `state destroy --all` batch prompt (issue #2247). Both already
2760
+ * carry this exact guard inline, and both differ in ways a shared helper would
2761
+ * have to grow parameters for — `destroy-runner.ts` has a default-YES bare
2762
+ * form alongside a default-NO `--remove-protection` form, and `state.ts`
2763
+ * passes an abort `signal` for the issue #2117 Ctrl-C handling. Folding them
2764
+ * in would also drag `destroy-runner.ts` (in the `integ-destroy` AND
2765
+ * `integ-broad` gate scopes) into a pure refactor's blast radius, buying a
2766
+ * real-AWS run for no behaviour change. The other five pre-existing guarded
2767
+ * prompts (`gc.ts`, `bootstrap-destroy.ts`, `recreate-confirm-prompt.ts`,
2768
+ * `prefix-migration-check.ts`, `migrate-command.ts`) stay put for the same
2769
+ * reason: each guards its own flow with its own error type.
2770
+ *
2771
+ * @param prompt the question, WITHOUT its trailing `[y/N]` marker
2772
+ * @returns `true` only for `y` / `yes` (any case, trimmed)
2773
+ */
2774
+ async function confirmOrRefuse(prompt, options) {
2775
+ if (process.stdin.isTTY !== true) throw new CdkdError(options.refusal, "NON_INTERACTIVE_CONFIRM");
2776
+ const rl = readline$1.createInterface({
2777
+ input: process.stdin,
2778
+ output: options.output ?? process.stdout
2779
+ });
2780
+ try {
2781
+ const ans = await rl.question(`${prompt}${options.suffix ?? " [y/N] "}`);
2782
+ return /^y(es)?$/i.test(ans.trim());
2783
+ } finally {
2784
+ rl.close();
2785
+ }
2786
+ }
2696
2787
 
2697
2788
  //#endregion
2698
2789
  //#region src/cli/commands/deployment-events-run.ts
@@ -60556,10 +60647,11 @@ var NestedStackProvider = class {
60556
60647
  const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
60557
60648
  this.logger.info(`Deploying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
60558
60649
  await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
60559
- const attributes = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
60650
+ const childOutputs = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
60560
60651
  return {
60561
60652
  physicalId: this.synthesizeArn(ctx.accountId, childRegion, ctx.parentStackName, logicalId),
60562
- attributes
60653
+ attributes: childOutputs.attributes,
60654
+ ...childOutputs.noEchoAttributeNames.length > 0 && { noEchoAttributeNames: childOutputs.noEchoAttributeNames }
60563
60655
  };
60564
60656
  }
60565
60657
  async update(logicalId, physicalId, _resourceType, properties, _previousProperties) {
@@ -60575,10 +60667,12 @@ var NestedStackProvider = class {
60575
60667
  const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
60576
60668
  this.logger.info(`Updating nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
60577
60669
  await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
60670
+ const updatedOutputs = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
60578
60671
  return {
60579
60672
  physicalId,
60580
60673
  wasReplaced: false,
60581
- attributes: await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion)
60674
+ attributes: updatedOutputs.attributes,
60675
+ ...updatedOutputs.noEchoAttributeNames.length > 0 && { noEchoAttributeNames: updatedOutputs.noEchoAttributeNames }
60582
60676
  };
60583
60677
  }
60584
60678
  async delete(logicalId, _physicalId, _resourceType, _properties, deleteContext) {
@@ -60651,10 +60745,50 @@ var NestedStackProvider = class {
60651
60745
  nestedTemplates: grandchildTemplates
60652
60746
  }, () => childEngine.deploy(childStackName, childTemplate));
60653
60747
  }
60748
+ /**
60749
+ * The child's outputs, as `Outputs.<Key>` attributes for the parent's
60750
+ * `Fn::GetAtt` — plus the subset whose plaintext this run had to RECOVER
60751
+ * (issue [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
60752
+ *
60753
+ * WHY THE RECOVERY IS HERE. This method reads the child's PERSISTED state,
60754
+ * and since #2274 an output whose value resolved a `NoEcho` custom resource's
60755
+ * `Data` is persisted as the redaction mask. Handing the parent `***` would
60756
+ * make the FIRST deploy of any such parent refuse — a template that deployed
60757
+ * before the feature — so where the child engine masked that output moments
60758
+ * ago IN THIS PROCESS, the plaintext is still available (see
60759
+ * `recoverMaskedOutput`) and is what the parent gets. The reported names are
60760
+ * what tells the deploy engine to re-register those values as MASK-ONLY
60761
+ * needles, so the parent's own record and every parent resource that consumes
60762
+ * one still persist `***`.
60763
+ *
60764
+ * PER ATTRIBUTE, not for the bag: a child typically exports one sensitive
60765
+ * output among several ordinary ones, and declaring the whole bag would mask
60766
+ * every unrelated output into the parent's state and into any parent resource
60767
+ * that reads one.
60768
+ *
60769
+ * A mask this run did NOT produce (the child was deployed by an earlier run
60770
+ * and its custom resource was `NO_CHANGE`, so no handler ran) is left as the
60771
+ * mask, and the resolver refuses the consumer rather than writing `***` to
60772
+ * AWS. That refusal is the feature's stated cost; recovering here removes it
60773
+ * for the in-run case only.
60774
+ */
60654
60775
  async readChildOutputsAsAttributes(ctx, childStackName, childRegion) {
60655
60776
  const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
60656
60777
  if (!childStateData) throw new Error(`Child stack state '${childStackName}' not found after deploy — NestedStackProvider invariant violated.`);
60657
- return this.buildOutputsAttributes(childStateData.state.outputs ?? {});
60778
+ const attributes = this.buildOutputsAttributes(childStateData.state.outputs ?? {});
60779
+ const noEchoAttributeNames = [];
60780
+ for (const [outputKey, persisted] of Object.entries(childStateData.state.outputs ?? {})) {
60781
+ if (!carriesSecretMask(persisted)) continue;
60782
+ const recovered = recoverMaskedOutput(childStackName, childRegion, outputKey);
60783
+ if (recovered === void 0) continue;
60784
+ const attributeName = this.outputAttributeName(outputKey);
60785
+ attributes[attributeName] = recovered;
60786
+ noEchoAttributeNames.push(attributeName);
60787
+ }
60788
+ return {
60789
+ attributes,
60790
+ noEchoAttributeNames
60791
+ };
60658
60792
  }
60659
60793
  requireContext() {
60660
60794
  const ctx = getCurrentNestedStackContext();
@@ -60777,9 +60911,17 @@ var NestedStackProvider = class {
60777
60911
  * make EVERY consumer persist it — but the count is "one resolution path",
60778
60912
  * not "one reader".
60779
60913
  */
60914
+ /**
60915
+ * The ONE spelling of a child output's attribute name. Both this class's
60916
+ * writers go through it, so the recovery pass (issue #2274) cannot name a key
60917
+ * `buildOutputsAttributes` did not write.
60918
+ */
60919
+ outputAttributeName(outputKey) {
60920
+ return `Outputs.${outputKey}`;
60921
+ }
60780
60922
  buildOutputsAttributes(outputs) {
60781
60923
  const attributes = {};
60782
- for (const [key, value] of Object.entries(outputs)) attributes[`Outputs.${key}`] = value;
60924
+ for (const [key, value] of Object.entries(outputs)) attributes[this.outputAttributeName(key)] = value;
60783
60925
  return attributes;
60784
60926
  }
60785
60927
  };
@@ -62830,6 +62972,36 @@ const TEMPLATE_BODY_LIMIT = CFN_TEMPLATE_BODY_LIMIT;
62830
62972
  */
62831
62973
  const TEMPLATE_URL_LIMIT = CFN_TEMPLATE_URL_LIMIT;
62832
62974
  /**
62975
+ * Best-effort reaper for the transient template bodies this flow uploads to
62976
+ * the state bucket under `cdkd-migrate-tmp/`.
62977
+ *
62978
+ * ONE implementation because {@link retireCloudFormationStack} drains from
62979
+ * four different exits — the recursive walk's `catch`, the confirmation
62980
+ * gate's decline AND its non-interactive refusal, the no-modification fast
62981
+ * path, and the post-`UpdateStack` `finally`. Each used to carry its own copy
62982
+ * of this loop, and the copies were how the refusal exit came to have none at
62983
+ * all: a `throw` from the gate passes every one of the other three.
62984
+ *
62985
+ * Best-effort by design. A leaked transient object costs pennies and lives
62986
+ * under an explicitly-named prefix, while a failed DELETE that propagated
62987
+ * would turn a successful retire into a reported failure. The retire's
62988
+ * success is governed by CloudFormation, not by S3 — so each cleanup's own
62989
+ * error becomes a `warn` naming the prefix to sweep manually.
62990
+ *
62991
+ * `what` / `when` keep the four sites' distinct wording (a caller says
62992
+ * whether these are the parent's own upload or a nested child's, and whether
62993
+ * it is unwinding); the SENTENCE they sit in is shared.
62994
+ */
62995
+ async function drainTemplateUploads(cleanups, stateBucket, labels) {
62996
+ const logger = getLogger();
62997
+ for (const cleanup of cleanups) try {
62998
+ await cleanup();
62999
+ } catch (cleanupErr) {
63000
+ const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
63001
+ logger.warn(`Failed to delete ${labels.what} from '${stateBucket}'${labels.when ? ` ${labels.when}` : ""}. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
63002
+ }
63003
+ }
63004
+ /**
62833
63005
  * Retire a CloudFormation stack whose resources have just been adopted into
62834
63006
  * cdkd state. The 4-step procedure is the one AWS recommends for handing
62835
63007
  * over resources between management tools without deleting them:
@@ -62893,35 +63065,31 @@ async function retireCloudFormationStack(options) {
62893
63065
  format = recursive.format;
62894
63066
  nestedCleanups.push(...recursive.cleanups);
62895
63067
  } catch (err) {
62896
- if (err instanceof RecursiveRetainInjectionError) for (const cleanup of err.cleanups) try {
62897
- await cleanup();
62898
- } catch (cleanupErr) {
62899
- const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
62900
- logger.warn(`Failed to delete partial nested-template upload from '${stateBucket}' during error recovery. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
62901
- }
63068
+ if (err instanceof RecursiveRetainInjectionError) await drainTemplateUploads(err.cleanups, stateBucket, {
63069
+ what: "partial nested-template upload",
63070
+ when: "during error recovery"
63071
+ });
62902
63072
  throw err;
62903
63073
  }
62904
63074
  } else ({body: newBody, modified, format} = injectRetainPolicies(tpl.TemplateBody, cfnStackName));
62905
63075
  if (!yes) {
62906
- if (!await confirmPrompt$6(`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`)) {
63076
+ let proceed = false;
63077
+ try {
63078
+ proceed = await confirmPrompt$6(`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`);
63079
+ } finally {
63080
+ if (!proceed) await drainTemplateUploads(nestedCleanups, stateBucket, {
63081
+ what: "temporary nested-template upload",
63082
+ when: "during cancel cleanup"
63083
+ });
63084
+ }
63085
+ if (!proceed) {
62907
63086
  logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
62908
- for (const cleanup of nestedCleanups) try {
62909
- await cleanup();
62910
- } catch (cleanupErr) {
62911
- const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
62912
- logger.warn(`Failed to delete temporary nested-template upload from '${stateBucket}' during cancel cleanup. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
62913
- }
62914
63087
  return { outcome: "cancelled" };
62915
63088
  }
62916
63089
  }
62917
63090
  if (!modified) {
62918
63091
  logger.info(`[2/4] Template already has Retain on every resource — skipping UpdateStack.`);
62919
- if (nestedCleanups.length > 0) for (const cleanup of nestedCleanups) try {
62920
- await cleanup();
62921
- } catch (cleanupErr) {
62922
- const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
62923
- logger.warn(`Failed to delete temporary nested-template upload from '${stateBucket}'. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
62924
- }
63092
+ await drainTemplateUploads(nestedCleanups, stateBucket, { what: "temporary nested-template upload" });
62925
63093
  } else {
62926
63094
  logger.info(`[2/4] Injected DeletionPolicy=Retain and UpdateReplacePolicy=Retain.`);
62927
63095
  if (newBody.length > TEMPLATE_URL_LIMIT) throw new Error(`Modified template is ${newBody.length} bytes, exceeds the CloudFormation UpdateStack TemplateURL limit (${TEMPLATE_URL_LIMIT}). cdkd state has already been written; retire the stack manually with (1) shrink the template, then (2) UpdateStack with Retain policies, (3) DeleteStack — or split the stack and retry.`);
@@ -62967,13 +63135,7 @@ async function retireCloudFormationStack(options) {
62967
63135
  maxDelay: 10
62968
63136
  }, { StackName: cfnStackName });
62969
63137
  } finally {
62970
- const allCleanups = [...s3Cleanup ? [s3Cleanup] : [], ...nestedCleanups];
62971
- for (const cleanup of allCleanups) try {
62972
- await cleanup();
62973
- } catch (cleanupErr) {
62974
- const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
62975
- logger.warn(`Failed to delete temporary template upload from '${stateBucket}'. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
62976
- }
63138
+ await drainTemplateUploads([...s3Cleanup ? [s3Cleanup] : [], ...nestedCleanups], stateBucket, { what: "temporary template upload" });
62977
63139
  }
62978
63140
  }
62979
63141
  logger.info(`[4/4] Deleting CloudFormation stack '${cfnStackName}' (resources retained)...`);
@@ -63284,17 +63446,30 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
63284
63446
  nested
63285
63447
  };
63286
63448
  }
63449
+ /**
63450
+ * The CloudFormation-stack retirement confirmation prompt, reached from
63451
+ * `cdkd import --migrate-from-cloudformation` and `cdkd migrate
63452
+ * --retire-cfn-stack` (both spell the skip flag `-y` / `--yes`). Its only call
63453
+ * site is inside the `if (!yes)` block above, which is what keeps
63454
+ * `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing on a
63455
+ * `--yes` run.
63456
+ *
63457
+ * TWO commands reach it, so the refusal NAMES BOTH. Every other site names one
63458
+ * command because it has one; a refusal that named neither would leave a CI
63459
+ * user knowing a flag is missing without knowing which invocation to put it
63460
+ * on, and `confirmOrRefuse`'s own contract is that the refusal is the only
63461
+ * thing that user sees.
63462
+ *
63463
+ * It also states that cdkd state has ALREADY been written. This prompt is the
63464
+ * only one of the nine that fires AFTER its command's state write, so a
63465
+ * refusal here leaves a genuine split brain — cdkd claims the resources while
63466
+ * the CloudFormation stack is still live — and the operator has to know that
63467
+ * before deciding whether to re-run or finish by hand.
63468
+ *
63469
+ * Exported for unit testing — internal to the command flow otherwise.
63470
+ */
63287
63471
  async function confirmPrompt$6(prompt) {
63288
- const rl = readline$1.createInterface({
63289
- input: process.stdin,
63290
- output: process.stdout
63291
- });
63292
- try {
63293
- const ans = await rl.question(`${prompt} [y/N] `);
63294
- return /^y(es)?$/i.test(ans.trim());
63295
- } finally {
63296
- rl.close();
63297
- }
63472
+ return confirmOrRefuse(prompt, { refusal: "The CloudFormation stack retirement confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes on whichever command you ran — cdkd import --migrate-from-cloudformation or cdkd migrate --retire-cfn-stack — or run it from a real terminal. NOTE cdkd state has already been written: the resources are recorded in cdkd state while the CloudFormation stack is still live, so re-run with -y / --yes (or retire the stack by hand) to finish the migration." });
63298
63473
  }
63299
63474
  /**
63300
63475
  * Flat `logicalId -> physicalId` map for ONE CloudFormation stack, or `null`
@@ -67724,6 +67899,37 @@ function collectDynamicReferencePaths(value, into, path = "") {
67724
67899
  if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectDynamicReferencePaths(v, into, path === "" ? k : `${path}.${k}`);
67725
67900
  }
67726
67901
  /**
67902
+ * Record the dotted path of every leaf that IS {@link SECRET_MASK} — the
67903
+ * mask-only twin of {@link collectDynamicReferencePaths} (issue
67904
+ * [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
67905
+ *
67906
+ * WITHOUT IT A NESTED MASK IS INVISIBLE, and that is a live disclosure rather
67907
+ * than a tidiness gap. `calculateResourceDrift` deliberately does not descend
67908
+ * arrays, so a masked leaf at `ContainerDefinitions.0.Environment.0.Value`
67909
+ * surfaces as ONE change at path `ContainerDefinitions` whose `stateValue` is
67910
+ * the WHOLE ARRAY — never equal to the mask. A whole-value equality test
67911
+ * therefore answers `false`, `secretBearing` stays false, the LIVE plaintext is
67912
+ * printed by the report, and `--accept` writes it into `state.json`, undoing
67913
+ * exactly the redaction this feature exists to perform.
67914
+ *
67915
+ * The expression class already answers this, and answers it this way: paths go
67916
+ * in at the LEAF's own coordinate and {@link isSecretBearingPath} bridges to the
67917
+ * comparator's coarser one by matching ANCESTORS. Seeded off the state bags,
67918
+ * exactly like the offline dynamic-reference seed, because a mask is a fact
67919
+ * about the RECORD and needs no AWS call to see.
67920
+ */
67921
+ function collectSecretMaskPaths(value, into, path = "") {
67922
+ if (typeof value === "string") {
67923
+ if (value === "***") into.add(path);
67924
+ return;
67925
+ }
67926
+ if (Array.isArray(value)) {
67927
+ value.forEach((item, i) => collectSecretMaskPaths(item, into, path === "" ? String(i) : `${path}.${i}`));
67928
+ return;
67929
+ }
67930
+ if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectSecretMaskPaths(v, into, path === "" ? k : `${path}.${k}`);
67931
+ }
67932
+ /**
67727
67933
  * The drift command's resolvers for ONE stack: the stack's own, plus one pinned
67728
67934
  * sibling per FOREIGN region an ARN-named reference asks for (issue
67729
67935
  * [#2108](https://github.com/go-to-k/cdkd/issues/2108)).
@@ -68093,7 +68299,7 @@ function redactDriftValue(value, secrets, secretBearing) {
68093
68299
  * cannot disagree — a `--dry-run` that promises a write the real run refuses is
68094
68300
  * worse than either behaviour on its own.
68095
68301
  *
68096
- * Two refusals, both about a value cdkd could not identify rather than about
68302
+ * Three refusals, all about a value cdkd could not identify rather than about
68097
68303
  * secrecy as such:
68098
68304
  *
68099
68305
  * - A masked VALUE says AWS holds something at a known-secret path that is not
@@ -68103,6 +68309,10 @@ function redactDriftValue(value, secrets, secretBearing) {
68103
68309
  * `setAtPath` would then create a key literally named `***` — and when
68104
68310
  * `awsValue` is `undefined` it would INSERT that key rather than removing the
68105
68311
  * real one, so the value check alone does not cover it.
68312
+ * - A masked BASELINE (issue #2274) says the state side itself is the mask —
68313
+ * a `NoEcho` custom resource's `Data` redacted out of `state.json`. Accepting
68314
+ * would write the live plaintext over a deliberate redaction, which is the
68315
+ * opposite of what the mask is for.
68106
68316
  *
68107
68317
  * The path check is what makes the value exemptions safe: `redactDriftValue`
68108
68318
  * lets `undefined` / `null` / a whole expression through unmasked, which is
@@ -68112,6 +68322,7 @@ function acceptRefusalReason(change, maskedPaths) {
68112
68322
  if (!maskedPaths.has(change.path)) return void 0;
68113
68323
  if (change.path.includes("***")) return "its property NAME came back carrying a secret value, so cdkd cannot name the key to write — and no redaction pass can clear it either, since they all walk values and never keys. ROTATE the secret; that is the only remedy";
68114
68324
  if (change.awsValue === void 0) return "AWS no longer reports it, and accepting an absence DELETES the key — which at or above a secret dynamic reference would erase the reference out of cdkd state. Use --revert to push it back, or re-deploy";
68325
+ if (change.stateValue === "***") return "cdkd does not know the value that belongs at this position — the baseline holds only the redaction mask, so accepting would write AWS-held plaintext over a deliberate redaction. Re-deploy to refresh it";
68115
68326
  return "it carries a secret dynamic reference and the value AWS currently holds is not the one the reference resolves to";
68116
68327
  }
68117
68328
  /**
@@ -68139,8 +68350,8 @@ function acceptRefusalReason(change, maskedPaths) {
68139
68350
  *
68140
68351
  * Returns the input array by identity when there is nothing to act on.
68141
68352
  */
68142
- function redactDriftChanges(changes, secrets, secretPaths) {
68143
- if (secrets.size === 0 && secretPaths.size === 0) return {
68353
+ function redactDriftChanges(changes, secrets, secretPaths, maskPaths) {
68354
+ if (secrets.size === 0 && secretPaths.size === 0 && maskPaths.size === 0 && !changes.some((change) => carriesSecretMask(change.stateValue))) return {
68144
68355
  changes,
68145
68356
  maskedPaths: /* @__PURE__ */ new Set()
68146
68357
  };
@@ -68148,9 +68359,10 @@ function redactDriftChanges(changes, secrets, secretPaths) {
68148
68359
  const redacted = [];
68149
68360
  for (const change of changes) {
68150
68361
  const maskedPath = maskSecretsInText(change.path, secrets);
68151
- const positionIsSecret = isSecretBearingPath(change.path, secretPaths);
68362
+ const positionIsSecret = isSecretBearingPath(change.path, secretPaths) || isSecretBearingPath(change.path, maskPaths);
68152
68363
  const nameCarriesSecret = maskedPath !== change.path;
68153
- const secretBearing = positionIsSecret || nameCarriesSecret;
68364
+ const stateValueIsMask = carriesSecretMask(change.stateValue);
68365
+ const secretBearing = positionIsSecret || nameCarriesSecret || stateValueIsMask;
68154
68366
  if (secretPaths.has(change.path) && change.awsValue === void 0) continue;
68155
68367
  const stateValue = redactDriftValue(change.stateValue, secrets, secretBearing);
68156
68368
  const awsValue = redactDriftValue(change.awsValue, secrets, secretBearing);
@@ -68247,6 +68459,9 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
68247
68459
  const seededSecretPaths = /* @__PURE__ */ new Set();
68248
68460
  collectDynamicReferencePaths(baseline, seededSecretPaths);
68249
68461
  if (useObserved) collectDynamicReferencePaths(resource.properties ?? {}, seededSecretPaths);
68462
+ const maskSecretPaths = /* @__PURE__ */ new Set();
68463
+ collectSecretMaskPaths(baseline, maskSecretPaths);
68464
+ if (useObserved) collectSecretMaskPaths(resource.properties ?? {}, maskSecretPaths);
68250
68465
  const unresolvedTokens = /* @__PURE__ */ new Set();
68251
68466
  const noteUnresolved = (tokens) => {
68252
68467
  for (const token of tokens) unresolvedTokens.add(token);
@@ -68287,7 +68502,7 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
68287
68502
  ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
68288
68503
  unionWalkObjects: useObserved,
68289
68504
  unorderedPaths
68290
- }), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths);
68505
+ }), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths, maskSecretPaths);
68291
68506
  const notComparedCause = secretResolutionFailed ? "refused" : unresolvedTokens.size > 0 ? "unresolvedToken" : void 0;
68292
68507
  if (reported.changes.length === 0) if (notComparedCause !== void 0) outcomes.push({
68293
68508
  kind: "notCompared",
@@ -68889,6 +69104,101 @@ function preserveLiveValuesAtUnresolvedTokens(send, awsProperties, secrets) {
68889
69104
  return walk(send, awsProperties);
68890
69105
  }
68891
69106
  /**
69107
+ * Keep `cdkd drift --revert` from pushing a REDACTION MASK to AWS (issue
69108
+ * [#2274](https://github.com/go-to-k/cdkd/issues/2274)).
69109
+ *
69110
+ * WHY THIS EXISTS AT ALL. A `NoEcho` custom resource's `Data` resolved into a
69111
+ * dependent's property is persisted as {@link SECRET_MASK}, because there is no
69112
+ * expression to store in its place. `--revert` pushes the BASELINE to AWS, so
69113
+ * without this a revert triggered by a SIBLING key would write the literal
69114
+ * `***` onto the live SSM parameter / secret / env var — the issue #1498 /
69115
+ * #1501 data-corruption class, and strictly worse than the disclosure the mask
69116
+ * exists to prevent. Shipping the mask without this guard would trade one for
69117
+ * the other.
69118
+ *
69119
+ * The sibling of {@link preserveLiveValuesAtUnresolvedTokens} and deliberately
69120
+ * a SEPARATE function rather than another branch inside it. That one is gated
69121
+ * on `unresolvedTokens.size > 0` at its call site, and this hazard is decided
69122
+ * by the SEND bag alone — a resource can carry a mask with no unresolved token
69123
+ * anywhere, so folding the two would put this check behind a condition that has
69124
+ * nothing to do with it.
69125
+ *
69126
+ * IT REGISTERS WHAT IT MOVES, exactly as the sibling does, and an earlier
69127
+ * revision's reason for not doing so was wrong: it said "the live value is one
69128
+ * cdkd never resolved and cannot recognise, so there is no needle to record".
69129
+ * The needle is `liveValue -> {@link SECRET_MASK}`, i.e. a MASK-ONLY entry, and
69130
+ * the evidence for it is the position — THE MASKED POSITION IS THE PROOF THE
69131
+ * VALUE IS SECRET, since nothing but this module's own redaction puts a mask at
69132
+ * a leaf. Without the registration this function copies live plaintext into the
69133
+ * send bag and then `collectNarrowedTopLevelKeys` persists that delta into
69134
+ * `observedProperties` against a `secrets` map holding no entry for it — the
69135
+ * same disclosure the mask exists to prevent, arriving through the mechanism
69136
+ * that is supposed to protect it — and the same omission un-masks it in
69137
+ * `maskSecretsInText(err.message, secrets)` and in the masker handed to
69138
+ * `provider.update`.
69139
+ *
69140
+ * `secrets` is therefore MUTATED, which is why it is a parameter rather than
69141
+ * something the caller could omit. `recordMaskOnlyValue` applies its own
69142
+ * refusals (an entry that already carries a real EXPRESSION wins; a plaintext
69143
+ * below the needle floor is not recorded), so this call site states only the
69144
+ * position fact and lets the module decide what is registrable.
69145
+ *
69146
+ * TWO OUTCOMES per masked leaf, and the second is why this returns a report
69147
+ * rather than just a bag:
69148
+ *
69149
+ * - AWS holds a value there -> copy the LIVE value in. The revert then leaves
69150
+ * that position exactly as AWS has it while still reverting every other
69151
+ * drifted key, which is the same trade `preserveLiveValuesAtUnresolvedTokens`
69152
+ * makes.
69153
+ * - AWS holds nothing there (the position could not be aligned, or the key is
69154
+ * absent live) -> there is no safe value to send, so the path is reported as
69155
+ * UNPRESERVABLE and the caller refuses the whole resource. Sending the mask
69156
+ * would be the corruption; dropping the key would delete a property the
69157
+ * resource may require.
69158
+ *
69159
+ * Array descent is POSITIONAL and bails to "no live value" on any length
69160
+ * mismatch, matching the sibling: a reordered readback cannot be positioned
69161
+ * against, and guessing would put one element's live value at another's index.
69162
+ *
69163
+ * Returns the input bag BY IDENTITY when it holds no mask, so the ordinary
69164
+ * revert is byte-identical and pays one walk.
69165
+ */
69166
+ function preserveLiveValuesAtMaskedLeaves(send, awsProperties, secrets) {
69167
+ const unpreservablePaths = [];
69168
+ let changed = false;
69169
+ const walk = (value, live, path) => {
69170
+ if (value === "***") {
69171
+ if (live === void 0) {
69172
+ unpreservablePaths.push(path);
69173
+ return value;
69174
+ }
69175
+ changed = true;
69176
+ if (typeof live === "string") recordMaskOnlyValue(secrets, live);
69177
+ return live;
69178
+ }
69179
+ if (Array.isArray(value)) {
69180
+ const liveItems = Array.isArray(live) && live.length === value.length ? live : void 0;
69181
+ return value.map((item, i) => walk(item, liveItems?.[i], `${path}[${i}]`));
69182
+ }
69183
+ if (value !== null && typeof value === "object") {
69184
+ const liveObject = isPlainRecord(live) ? live : void 0;
69185
+ const out = {};
69186
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject?.[k], path === "" ? k : `${path}.${k}`);
69187
+ return out;
69188
+ }
69189
+ return value;
69190
+ };
69191
+ const properties = walk(send, awsProperties, "");
69192
+ if (!changed && unpreservablePaths.length === 0) return {
69193
+ properties: send,
69194
+ unpreservablePaths
69195
+ };
69196
+ return {
69197
+ properties,
69198
+ unpreservablePaths
69199
+ };
69200
+ }
69201
+ /**
68892
69202
  * Build the `newProperties` object passed to `provider.update` during
68893
69203
  * `--revert`. Strategy:
68894
69204
  *
@@ -69130,7 +69440,13 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
69130
69440
  let newProperties;
69131
69441
  try {
69132
69442
  const overlaid = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
69133
- newProperties = unresolvedTokens.size > 0 ? preserveLiveValuesAtUnresolvedTokens(overlaid, outcome.awsProperties, secrets) : overlaid;
69443
+ const maskPreserved = preserveLiveValuesAtMaskedLeaves(unresolvedTokens.size > 0 ? preserveLiveValuesAtUnresolvedTokens(overlaid, outcome.awsProperties, secrets) : overlaid, outcome.awsProperties, secrets);
69444
+ if (maskPreserved.unpreservablePaths.length > 0) {
69445
+ totalUnresolvable++;
69446
+ logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): refused to revert ${maskPreserved.unpreservablePaths.join(", ")} — the recorded baseline holds only the redaction mask there (a NoEcho custom-resource value), and AWS reports nothing to preserve, so cdkd has no value it may write. Force that custom resource to update (change one of its properties, e.g. a nonce) and re-deploy, so its handler runs again and supplies the value — an ordinary re-deploy leaves it unchanged and the mask stays.`);
69447
+ return;
69448
+ }
69449
+ newProperties = maskPreserved.properties;
69134
69450
  } catch (err) {
69135
69451
  totalFailed++;
69136
69452
  logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not build the revert payload — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
@@ -69335,24 +69651,26 @@ async function runWithConcurrency(tasks, concurrency) {
69335
69651
  await Promise.all(workers);
69336
69652
  }
69337
69653
  /**
69338
- * Ask the operator to confirm a mutation.
69654
+ * `cdkd drift --accept` / `--revert`'s confirmation prompt: ask the operator to
69655
+ * confirm a mutation. Both call sites sit inside an `if (!options.yes)` block,
69656
+ * which is what keeps `confirmOrRefuse`'s non-interactive refusal (issue
69657
+ * #2275) from firing on a `--yes` run.
69658
+ *
69659
+ * Issue #2230 is why this site passes an explicit `output`: the prompt is
69660
+ * written to `out.stream`, not unconditionally to `process.stdout` — under
69661
+ * `--json` that stream carries the payload, and a bare `[y/N] ` spliced into
69662
+ * it is the same corruption as a status line, so the sink is `process.stderr`
69663
+ * there. The prompt is still SHOWN; only its stream changes. See the
69664
+ * `HumanTextSink` doc above for why the STREAM (not a closure) is what gets
69665
+ * handed to `createInterface`.
69339
69666
  *
69340
- * Issue #2230: the prompt is written to `out.stream`, not unconditionally to
69341
- * `process.stdout` — under `--json` that stream carries the payload, and a
69342
- * bare `[y/N] ` spliced into it is the same corruption as a status line.
69343
- * The prompt is still SHOWN; only its stream changes.
69667
+ * Exported for unit testing internal to the command flow otherwise.
69344
69668
  */
69345
69669
  async function confirmPrompt$5(prompt, out) {
69346
- const rl = readline$1.createInterface({
69347
- input: process.stdin,
69348
- output: out.stream
69670
+ return confirmOrRefuse(prompt, {
69671
+ output: out.stream,
69672
+ refusal: "The cdkd drift confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm, or run the command from a real terminal."
69349
69673
  });
69350
- try {
69351
- const ans = await rl.question(`${prompt} [y/N] `);
69352
- return /^y(es)?$/i.test(ans.trim());
69353
- } finally {
69354
- rl.close();
69355
- }
69356
69674
  }
69357
69675
  function writeJsonReport(reports) {
69358
69676
  const payload = reports.map((r) => {
@@ -70083,17 +70401,15 @@ async function emptyBucketAllVersions(s3, bucket) {
70083
70401
  versionIdMarker = resp.NextVersionIdMarker;
70084
70402
  } while (keyMarker || versionIdMarker);
70085
70403
  }
70404
+ /**
70405
+ * `cdkd state migrate`'s confirmation prompt. Its only call site is inside the
70406
+ * `if (!options.yes)` block above, which is what keeps `confirmOrRefuse`'s
70407
+ * non-interactive refusal (issue #2275) from firing on a `--yes` run.
70408
+ *
70409
+ * Exported for unit testing — internal to the command flow otherwise.
70410
+ */
70086
70411
  async function confirmPrompt$4(prompt) {
70087
- const rl = readline$1.createInterface({
70088
- input: process.stdin,
70089
- output: process.stdout
70090
- });
70091
- try {
70092
- const ans = await rl.question(`${prompt} [y/N] `);
70093
- return /^y(es)?$/i.test(ans.trim());
70094
- } finally {
70095
- rl.close();
70096
- }
70412
+ return confirmOrRefuse(prompt, { refusal: "The cdkd state migrate confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm the migration, or run the command from a real terminal." });
70097
70413
  }
70098
70414
  /**
70099
70415
  * Create the `cdkd state migrate` subcommand.
@@ -72010,6 +72326,14 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
72010
72326
  });
72011
72327
  continue;
72012
72328
  }
72329
+ if (carriesSecretMask(stateEntry.properties)) {
72330
+ blocked.push({
72331
+ logicalId,
72332
+ resourceType,
72333
+ reason: "cdkd state holds only the redaction mask ('***') for at least one property — a NoEcho custom-resource value cdkd cannot re-derive. Forcing the custom resource to update does NOT clear this: the handler supplies the value to the deploy, and cdkd re-masks it on the way into state, so the export still has nothing to declare. Stop setting NoEcho on that response and re-deploy, then export again; or export this stack without that resource and adopt it into CloudFormation by hand. See https://github.com/go-to-k/cdkd/issues/2274."
72334
+ });
72335
+ continue;
72336
+ }
72013
72337
  if (IMPORT_UNSUPPORTED_RECREATABLE_TYPES.has(resourceType)) {
72014
72338
  if (options.recreateImportUnsupported) recreateBeforePhase2.push({
72015
72339
  logicalId,
@@ -73705,17 +74029,18 @@ function printNextSteps(args) {
73705
74029
  }
73706
74030
  logger.info("");
73707
74031
  }
74032
+ /**
74033
+ * `cdkd export`'s confirmation prompt, shared by its THREE call sites (the
74034
+ * rollback-journal override, the single-stack migration confirm, and the
74035
+ * nested-stack tree-wide confirm in `runPerStackImportLoop`). Every one of
74036
+ * them sits inside an `if (!options.yes ...)` block, which is what keeps
74037
+ * `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing on a
74038
+ * `--yes` run.
74039
+ *
74040
+ * Exported for unit testing — internal to the export flow otherwise.
74041
+ */
73708
74042
  async function confirmPrompt$3(prompt) {
73709
- const rl = readline$1.createInterface({
73710
- input: process.stdin,
73711
- output: process.stdout
73712
- });
73713
- try {
73714
- const ans = await rl.question(`${prompt} [y/N] `);
73715
- return /^y(es)?$/i.test(ans.trim());
73716
- } finally {
73717
- rl.close();
73718
- }
74043
+ return confirmOrRefuse(prompt, { refusal: "The cdkd export confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm, or run the command from a real terminal." });
73719
74044
  }
73720
74045
  function createExportCommand() {
73721
74046
  const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.").option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--skip-import-support-preflight", "Skip the pre-flight that refuses resource types whose CloudFormation registry schema says CFn cannot IMPORT them (no read handler AND NON_PROVISIONABLE), and let CreateChangeSet answer instead. The pre-flight is a registry HEURISTIC, not AWS's supported-for-import list, so this is the escape hatch for a type AWS has since made importable. Expect the changeset to be rejected if the heuristic was right.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
@@ -73739,6 +74064,26 @@ function formatStackRef(ref) {
73739
74064
  return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
73740
74065
  }
73741
74066
  /**
74067
+ * {@link formatStackRef} for a sentence a CONFIRMATION PROMPT renders.
74068
+ *
74069
+ * Both halves come from an S3 key segment (or, for a legacy record, a state
74070
+ * body), so both are attacker-influenced in exactly the way issue #2170 round
74071
+ * 4 found for the lock error sixty lines below this helper's callers: a
74072
+ * planted `\n` forged a whole line inside that sentence. A forged line in a
74073
+ * PROMPT is strictly worse than one in an error, because the sentence it
74074
+ * corrupts is the one the operator answers `y` to.
74075
+ *
74076
+ * `asciiOnly` matches the lock error's own call on these same values — a
74077
+ * stack name and an AWS region both have a known charset — and is a no-op on
74078
+ * every ordinary input, so the shipped prompt strings are byte-identical.
74079
+ * `UNRENDERABLE` stands in when sanitising leaves nothing, since an empty
74080
+ * `()` would read as "no region" rather than "a region cdkd will not print".
74081
+ */
74082
+ function formatStackRefSafe(ref) {
74083
+ const stack = displaySafe(ref.stackName, { asciiOnly: true }) || "<unrenderable>";
74084
+ return ref.region ? `${stack} (${displaySafe(ref.region, { asciiOnly: true }) || "<unrenderable>"})` : stack;
74085
+ }
74086
+ /**
73742
74087
  * Resolve a stack name + optional region flag against the `listStacks` index
73743
74088
  * built up front. When a name resolves to multiple regions and the caller
73744
74089
  * didn't pin one, surface a clear error listing the candidates so the user
@@ -73842,7 +74187,7 @@ function sortRefs(refs) {
73842
74187
  async function stateListCommand(options) {
73843
74188
  const logger = getLogger();
73844
74189
  if (options.verbose) logger.setLevel("debug");
73845
- if (options.json) reserveStdoutForPayload();
74190
+ reserveStdoutForPayload();
73846
74191
  const setup = await setupStateBackend(options);
73847
74192
  try {
73848
74193
  const refs = sortRefs(await setup.stateBackend.listStacks());
@@ -74311,16 +74656,9 @@ async function stateOrphanCommand(stackArgs, options) {
74311
74656
  }
74312
74657
  }
74313
74658
  if (!options.yes && !options.force) {
74314
- const targetList = targets.map((t) => formatStackRef(t)).join(", ");
74659
+ const targetList = targets.map((t) => formatStackRefSafe(t)).join(", ");
74315
74660
  process.stdout.write(`\nWARNING: This removes cdkd's state record for [${targetList}] only. AWS resources will NOT be deleted.\nUse 'cdkd destroy ${stackName}' if you want to delete the actual resources.\n\n`);
74316
- const rl = readline$1.createInterface({
74317
- input: process.stdin,
74318
- output: process.stdout
74319
- });
74320
- const answer = await rl.question(`Remove state for ${targetList} from s3://${setup.bucket}/${setup.prefix}/? (y/N): `);
74321
- rl.close();
74322
- const trimmed = answer.trim().toLowerCase();
74323
- if (trimmed !== "y" && trimmed !== "yes") {
74661
+ if (!await confirmStateOrphanRemoval(`Remove state for ${targetList} from s3://${setup.bucket}/${setup.prefix}/?`)) {
74324
74662
  logger.info(`Cancelled removal of state for stack: ${stackName}`);
74325
74663
  continue;
74326
74664
  }
@@ -74886,7 +75224,7 @@ async function stateRefreshObservedCommand(stackArgs, options) {
74886
75224
  }
74887
75225
  }
74888
75226
  if (!options.yes && !options.dryRun) {
74889
- const targetList = targets.map(formatStackRef).join(", ");
75227
+ const targetList = targets.map(formatStackRefSafe).join(", ");
74890
75228
  if (!await confirmRefresh(`Refresh observedProperties for ${targets.length} stack(s) (${targetList})?`)) {
74891
75229
  logger.info("Aborted.");
74892
75230
  return;
@@ -75067,17 +75405,34 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
75067
75405
  });
75068
75406
  }
75069
75407
  }
75070
- async function confirmRefresh(prompt) {
75071
- const rl = readline$1.createInterface({
75072
- input: process.stdin,
75073
- output: process.stdout
75408
+ /**
75409
+ * `cdkd state orphan`'s confirmation prompt. Its only call site is inside the
75410
+ * `if (!options.yes && !options.force)` block in `stateOrphanCommand`, which
75411
+ * is what keeps `confirmOrRefuse`'s non-interactive refusal (issue #2275)
75412
+ * from firing on a flagged run.
75413
+ *
75414
+ * The `(y/N): ` suffix is preserved verbatim from before issue #2275 folded
75415
+ * the guard in: it is user-visible output, and only this prompt and
75416
+ * `cdkd rollback` ever spelled it that way.
75417
+ *
75418
+ * Exported for unit testing — internal to the state-orphan flow otherwise.
75419
+ */
75420
+ async function confirmStateOrphanRemoval(prompt) {
75421
+ return confirmOrRefuse(prompt, {
75422
+ suffix: " (y/N): ",
75423
+ refusal: "The cdkd state orphan confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes (or -f / --force) to confirm the removal, or run the command from a real terminal."
75074
75424
  });
75075
- try {
75076
- const ans = await rl.question(`${prompt} [y/N] `);
75077
- return /^y(es)?$/i.test(ans.trim());
75078
- } finally {
75079
- rl.close();
75080
- }
75425
+ }
75426
+ /**
75427
+ * `cdkd state refresh-observed`'s confirmation prompt. Its only call site is
75428
+ * inside the `if (!options.yes && !options.dryRun)` block above, which is what
75429
+ * keeps `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing
75430
+ * on a `--yes` run.
75431
+ *
75432
+ * Exported for unit testing — internal to the refresh-observed flow otherwise.
75433
+ */
75434
+ async function confirmRefresh(prompt) {
75435
+ return confirmOrRefuse(prompt, { refusal: "The cdkd state refresh-observed confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm the refresh, or run the command from a real terminal." });
75081
75436
  }
75082
75437
  /**
75083
75438
  * Create the `state refresh-observed` subcommand.
@@ -75208,17 +75563,20 @@ function failedActionLabel(item, skipFinalSnapshot) {
75208
75563
  case "skip-failed-absent": return ` - skip ${op.logicalId} (${op.resourceType}) — no previous state available`;
75209
75564
  }
75210
75565
  }
75566
+ /**
75567
+ * `cdkd rollback`'s confirmation prompt. Exported for unit testing — internal
75568
+ * to the rollback flow otherwise, whose only call site is inside the
75569
+ * `if (!skipConfirmation)` block below.
75570
+ *
75571
+ * The `(y/N): ` suffix is preserved verbatim from before issue #2275 folded
75572
+ * the non-interactive guard into `confirmOrRefuse`: it is user-visible output,
75573
+ * and only this site and `cdkd state orphan` ever spelled it that way.
75574
+ */
75211
75575
  async function confirm(question) {
75212
- const rl = readline$1.createInterface({
75213
- input: process.stdin,
75214
- output: process.stdout
75576
+ return confirmOrRefuse(question, {
75577
+ suffix: " (y/N): ",
75578
+ refusal: "The cdkd rollback confirmation prompt cannot run in a non-interactive environment. Pass --force (or -y / --yes) to confirm the rollback, or run the command from a real terminal."
75215
75579
  });
75216
- try {
75217
- const t = (await rl.question(`${question} (y/N): `)).trim().toLowerCase();
75218
- return t === "y" || t === "yes";
75219
- } finally {
75220
- rl.close();
75221
- }
75222
75580
  }
75223
75581
  async function rollbackCommand(stackArg, options) {
75224
75582
  const logger = getLogger();
@@ -77691,17 +78049,16 @@ function stringifyForAudit(value) {
77691
78049
  if (typeof value === "string") return JSON.stringify(value);
77692
78050
  return JSON.stringify(value);
77693
78051
  }
78052
+ /**
78053
+ * `cdkd orphan`'s confirmation prompt. Its only call site is inside the
78054
+ * `if (!options.yes && !options.force)` block above, which is what keeps
78055
+ * `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing on a
78056
+ * flagged run.
78057
+ *
78058
+ * Exported for unit testing — internal to the command flow otherwise.
78059
+ */
77694
78060
  async function confirmPrompt$1(prompt) {
77695
- const rl = readline$1.createInterface({
77696
- input: process.stdin,
77697
- output: process.stdout
77698
- });
77699
- try {
77700
- const ans = await rl.question(`${prompt} [y/N] `);
77701
- return /^y(es)?$/i.test(ans.trim());
77702
- } finally {
77703
- rl.close();
77704
- }
78061
+ return confirmOrRefuse(prompt, { refusal: "The cdkd orphan confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes (or -f / --force) to confirm the orphan, or run the command from a real terminal." });
77705
78062
  }
77706
78063
  /**
77707
78064
  * Create the top-level `cdkd orphan` command.
@@ -78714,17 +79071,15 @@ function formatOutcome(outcome) {
78714
79071
  case "failed": return "✗";
78715
79072
  }
78716
79073
  }
79074
+ /**
79075
+ * `cdkd import`'s confirmation prompt. Its only call site is inside the
79076
+ * `if (!options.yes)` block above, which is what keeps `confirmOrRefuse`'s
79077
+ * non-interactive refusal (issue #2275) from firing on a `--yes` run.
79078
+ *
79079
+ * Exported for unit testing — internal to the command flow otherwise.
79080
+ */
78717
79081
  async function confirmPrompt(prompt) {
78718
- const rl = readline$1.createInterface({
78719
- input: process.stdin,
78720
- output: process.stdout
78721
- });
78722
- try {
78723
- const ans = await rl.question(`${prompt} [y/N] `);
78724
- return /^y(es)?$/i.test(ans.trim());
78725
- } finally {
78726
- rl.close();
78727
- }
79082
+ return confirmOrRefuse(prompt, { refusal: "The cdkd import confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm the state write, or run the command from a real terminal." });
78728
79083
  }
78729
79084
  /**
78730
79085
  * Create the `cdkd import` top-level command.
@@ -89038,4 +89393,4 @@ function buildProgram() {
89038
89393
 
89039
89394
  //#endregion
89040
89395
  export { buildProgram };
89041
- //# sourceMappingURL=program-CjlLza5f.js.map
89396
+ //# sourceMappingURL=program-B9vnKc3N.js.map