@go-to-k/cdkd 0.285.16 → 0.286.1

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 endCommandInterruptScope, $n as resolveCaptureObservedState, $r as isThrottlingError, $t as maskSecretsInText, A as bold, An as getBootstrapMarkerKey, Ar as DeployCancelledError, At as classifyReplaySecretRegion, B as secretBearingStateKeyWarning, Bn as formatDockerLoginError, Br as ProvisioningError, Bt as describeTypeWithThrottleRetry, C as unsupportedFinalSnapshotError, Cn as rewriteTemplateAssetReferences, Cr as resetAwsClients, Ct as configBooleanRefusal, D as isStatefulRecreateTargetSync, Dn as BOOTSTRAP_MARKER_PREFIX, Dt as requireConfigArray, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as AssetModeResolver, Er as CdkdError, Et as replayWarn, F as yellow, Fn as validateContainerRepoName, Fr as LocalStartServiceError, Ft as s3BucketRegionalDomainName, G as clearOnUpdateRemoval, Gn as AssetManifestLoader, Gt as STATE_SOURCED_READBACK_RULES, H as getCurrentResourceSecrets, Hn as partitionSensitiveEnv, Hr as ResourceUpdateNotSupportedError, Ht as DagBuilder, I as collectDeclaredOutputNames, In as buildDenyExternalAccessPolicy, It as s3BucketWebsiteUrl, J as findSilentDropProperties, Jn as synthesisStatusMessage, Jt as createSecretMasker, K as ProviderRegistry, Kn as getDockerImageBySourceHash, Kt as TEMPLATE_SOURCED_RULES, L as collectPublishedOutputNames, Ln as describeAwsFailure, Lr as MissingCdkCliError, Lt as applyRoleArnIfSet, M as gray, Mn as parseBootstrapMarker, Mr as IntrinsicResolutionRefusalError, Mt as s3BucketArn, N as green, Nn as readBootstrapMarkerBody, Nr as LocalInvokeBuildError$1, Nt as s3BucketDomainName, O as renderStatefulReason, On as assertAssetBucketRegion, Or as CrossAccountSecretRefusalError, Ot as requireConfigObject, P as red, Pn as validateAssetBucketName, Pr as LocalMigrateError, Pt as s3BucketDualStackDomainName, Q as beginCommandInterruptScope, Qn as resolveAutoAssetStorage, Qr as isRetryableTransientError, Qt as maskSecretsInError, R as exportAliasCollisionScrubWarning, Rn as buildDockerImage, Rr as NestedStackChildDirectDestroyError, Rt as DiffCalculator, S as refusesFinalSnapshot, Sn as loadPublishableAssetManifest, Sr as getAwsClients, St as coerceCfnBoolean, T as extractDeploymentEventError, Tn as stripControlChars, Tt as readConfigString, U as IAMRoleProvider, Un as runDockerForeground, Ur as StackHasActiveImportsError, Ut as TemplateParser, V as stateKeySecretExposure, Vn as getDockerCmd, Vr as ResourceTimeoutError, Vt as withRetry, W as collectInlinePolicyNamesManagedBySiblings, Wn as runDockerStreaming, Wr as StackTerminationProtectionError, Wt as STATE_SOURCED_CROSS_GENERATION_RULES, X as maskDeep, Xn as getLegacyStateBucketName, Xr as withErrorHandling, Xt as errorCauseChain, Y as createMaskedRetryLogger, Yn as getDefaultStateBucketName, Yr as normalizeAwsError, Yt as dynamicReferenceTokens, Z as maskerOrIdentity, Zn as resolveApp, Zr as isMarkedNonRetryable, Zt as isSingleDynamicReferenceToken, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as AssetPublisher, _r as AssemblyReader, _t as refStateLookupFromResource, a as DeploymentEventsStore, an as S3StateBackend, ar as warnDeprecatedNoPrefixCliFlag, at as UNSPECIFIED_SKIP_REASON, b as createPreDeleteFinalSnapshot, bn as buildAssetRedirectMap, br as resolveBucketRegion, bt as resolveExplicitPhysicalId, c as replayFailedOperations, cn as buildForceUnlockCommand, cr as MIGRATE_TMP_PREFIX, ct as disableInstanceApiTermination, d as updatePartialReason, dn as CUSTOM_RESOURCE_RESPONSE_PREFIX, dr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, dt as carriesDynamicReference, ei as isTransientServerError, en as recordMaskOnlyValue, er as resolveSkipPrefix, et as interruptWatchListenerCount, f as withResourceDeadline, fn as DEFAULT_STATE_PREFIX, fr as displaySafe, ft as cfnRefValueFromPhysicalId, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as shouldRetainResource, gr as derivePartitionAndUrlSuffix, gt as parameterTypeMayLoseSecretIdentity, h as computeImplicitDeleteEdges, hn as importableOutputs, hr as canonicalizeRegion, ht as isUnboundTemplateParameter, i as DeploymentEventsReader, in as LockManager, ir as stateBucketExistenceConfirmed, it as slowCcOperationTimeoutMs, j as cyan, jn as isCrossRegionRedirect, jr as DynamicReferenceRegionAmbiguousError, jt as producerRegionsFromState, k as formatResourceLine, kn as ensureAssetStorage, kt as requireConfigString, l as replayRollback, ln as buildLockContentionMessage, lr as findLargeInlineResources, lt as isTerminationProtectionPropagationError, m as IMPLICIT_DELETE_DEPENDENCIES, mn as importableOutputKeys, mr as PARTITION_TABLE, mt as getAccountInfo, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as markRedactedCause, nn as redactSecretsForState, nr as resolveStateBucketWithDefaultAndSource, nt as startInterruptWatch, o as planFailedOps, on as rebuildClientForBucketRegion, or as CFN_TEMPLATE_BODY_LIMIT, ot as deleteIndeterminateGuards, p as maskingRetryLogger, pn as exportNamesCarriedFrom, pr as expectedOwnerParam, pt as coerceParameterTypedValue, q as findActionableSilentDrops, qn as Synthesizer, qt as carriesSecretMask, r as DeployEngine, ri as retryClassificationText, rn as scrubResourceRecord, rr as resolveUseCdkBootstrapAssets, rt as CloudControlProvider, s as planRollback, sn as UNRENDERABLE, sr as CFN_TEMPLATE_URL_LIMIT, st as deleteSkipReason, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as markNonRetryable, tn as recoverMaskedOutput, tr as resolveStateBucketWithDefault, tt as isInterruptedWaitError, u as updatePartialMessage, un as forceQuitRecoveryClause, ur as uploadCfnTemplate, ut as IntrinsicFunctionResolver, v as buildFinalSnapshotIdentifier, vn as stringifyValue, vr as processStackMessages, vt as WAFv2WebACLProvider, w as makeCanonicalizePropertiesFn, wn as escapeRegExp, wr as setAwsClients, wt as configStringRefusal, x as isFinalSnapshotError, xn as createAssetRedirectResolver, xr as AwsClients, xt as assertRegionMatch, y as ccRoutedFinalSnapshotError, yn as WorkGraph, yt as normalizeAwsTagsToCfn, z as isExportAliasCollision, zn as dockerSpawnEnvWithSensitive, zr as PartialFailureError, zt as INTRINSIC_KEYS } from "./deploy-engine-DuFUy9D0.js";
1
+ import { $ as beginCommandInterruptScope, $n as resolveAutoAssetStorage, $r as isTransientServerError, $t as maskSecretsInError, A as formatResourceLine, An as ensureAssetStorage, At as requireConfigString, B as isExportAliasCollision, Bn as dockerSpawnEnvWithSensitive, Br as ResourceTimeoutError, Bt as INTRINSIC_KEYS, C as unsupportedFinalSnapshotError, Cn as loadPublishableAssetManifest, Cr as getAwsClients, Ct as coerceCfnBoolean, D as isStatefulRecreateTargetForReplace, Dn as AssetModeResolver, Dr as CdkdError, Dt as replayWarn, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as stripControlChars, Et as readConfigString, F as red, Fn as validateAssetBucketName, Fr as LocalStartServiceError, Ft as s3BucketDualStackDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as runDockerStreaming, Gt as STATE_SOURCED_CROSS_GENERATION_RULES, H as stateKeySecretExposure, Hn as getDockerCmd, Hr as StackHasActiveImportsError, Ht as withRetry, I as yellow, In as validateContainerRepoName, It as s3BucketRegionalDomainName, J as findActionableSilentDrops, Jn as Synthesizer, Jr as normalizeAwsError, Jt as carriesSecretMask, K as clearOnUpdateRemoval, Kn as AssetManifestLoader, Kt as STATE_SOURCED_READBACK_RULES, L as collectDeclaredOutputNames, Ln as buildDenyExternalAccessPolicy, Lr as NestedStackChildDirectDestroyError, Lt as s3BucketWebsiteUrl, M as cyan, Mn as isCrossRegionRedirect, Mr as DynamicReferenceRegionAmbiguousError, Mt as producerRegionsFromState, N as gray, Nn as parseBootstrapMarker, Nr as IntrinsicResolutionRefusalError, Nt as s3BucketArn, O as isStatefulRecreateTargetSync, On as BOOTSTRAP_MARKER_PREFIX, Ot as requireConfigArray, P as green, Pn as readBootstrapMarkerBody, Pr as LocalInvokeBuildError$1, Pt as s3BucketDomainName, Q as maskerOrIdentity, Qn as resolveApp, Qr as isThrottlingError, Qt as isSingleDynamicReferenceToken, R as collectPublishedOutputNames, Rn as describeAwsFailure, Rr as PartialFailureError, Rt as applyRoleArnIfSet, S as refusesFinalSnapshot, Sn as createAssetRedirectResolver, Sr as AwsClients, St as assertRegionMatch, T as extractDeploymentEventError, Tn as escapeRegExp, Tr as setAwsClients, Tt as configStringRefusal, U as getCurrentResourceSecrets, Un as partitionSensitiveEnv, Ur as StackTerminationProtectionError, Ut as DagBuilder, V as secretBearingStateKeyWarning, Vn as formatDockerLoginError, Vr as ResourceUpdateNotSupportedError, Vt as describeTypeWithThrottleRetry, W as IAMRoleProvider, Wn as runDockerForeground, Wt as TemplateParser, X as createMaskedRetryLogger, Xn as getDefaultStateBucketName, Xr as isMarkedNonRetryable, Xt as dynamicReferenceTokens, Y as findSilentDropProperties, Yn as synthesisStatusMessage, Yr as withErrorHandling, Yt as createSecretMasker, Z as maskDeep, Zn as getLegacyStateBucketName, Zr as isRetryableTransientError, Zt as errorCauseChain, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as shouldRetainResource, _r as derivePartitionAndUrlSuffix, _t as parameterTypeMayLoseSecretIdentity, a as DeploymentEventsStore, an as LockManager, ar as stateBucketExistenceConfirmed, at as slowCcOperationTimeoutMs, b as createPreDeleteFinalSnapshot, bn as WorkGraph, bt as normalizeAwsTagsToCfn, c as replayFailedOperations, cn as UNRENDERABLE, cr as CFN_TEMPLATE_URL_LIMIT, ct as deleteSkipReason, d as updatePartialReason, dn as forceQuitRecoveryClause, dr as uploadCfnTemplate, dt as IntrinsicFunctionResolver, ei as markNonRetryable, en as maskSecretsInText, er as resolveCaptureObservedState, et as endCommandInterruptScope, f as withResourceDeadline, fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, fr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, ft as carriesDynamicReference, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as importableOutputs, gr as canonicalizeRegion, gt as isUnboundTemplateParameter, h as computeImplicitDeleteEdges, hn as importableOutputKeys, hr as PARTITION_TABLE, ht as getAccountInfo, i as DeploymentEventsReader, in as scrubResourceRecord, ir as resolveUseCdkBootstrapAssets, it as CloudControlProvider, j as bold, jn as getBootstrapMarkerKey, jr as DeployCancelledError, jt as classifyReplaySecretRegion, k as renderStatefulReason, kn as assertAssetBucketRegion, kr as CrossAccountSecretRefusalError, kt as requireConfigObject, l as replayRollback, ln as buildForceUnlockCommand, lr as MIGRATE_TMP_PREFIX, lt as disableInstanceApiTermination, m as IMPLICIT_DELETE_DEPENDENCIES, mn as exportNamesCarriedFrom, mr as expectedOwnerParam, mt as coerceParameterTypedValue, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as retryClassificationText, nn as recoverMaskedOutput, nr as resolveStateBucketWithDefault, nt as isInterruptedWaitError, o as planFailedOps, on as S3StateBackend, or as warnDeprecatedNoPrefixCliFlag, ot as UNSPECIFIED_SKIP_REASON, p as maskingRetryLogger, pn as DEFAULT_STATE_PREFIX, pr as displaySafe, pt as cfnRefValueFromPhysicalId, q as ProviderRegistry, qn as getDockerImageBySourceHash, qt as TEMPLATE_SOURCED_RULES, r as DeployEngine, rn as redactSecretsForState, rr as resolveStateBucketWithDefaultAndSource, rt as startInterruptWatch, s as planRollback, sn as rebuildClientForBucketRegion, sr as CFN_TEMPLATE_BODY_LIMIT, st as deleteIndeterminateGuards, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as markRedactedCause, tn as recordMaskOnlyValue, tr as resolveSkipPrefix, tt as interruptWatchListenerCount, u as updatePartialMessage, un as buildLockContentionMessage, ur as findLargeInlineResources, ut as isTerminationProtectionPropagationError, v as buildFinalSnapshotIdentifier, vn as AssetPublisher, vr as AssemblyReader, vt as refStateLookupFromResource, w as makeCanonicalizePropertiesFn, wn as rewriteTemplateAssetReferences, wr as resetAwsClients, wt as configBooleanRefusal, x as isFinalSnapshotError, xn as buildAssetRedirectMap, xr as resolveBucketRegion, xt as resolveExplicitPhysicalId, y as ccRoutedFinalSnapshotError, yn as stringifyValue, yr as processStackMessages, yt as WAFv2WebACLProvider, z as exportAliasCollisionScrubWarning, zn as buildDockerImage, zr as ProvisioningError, zt as DiffCalculator } from "./deploy-engine-D0RCEQ6D.js";
2
2
  import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
3
3
  import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
4
- import { t as getCdkdVersion } from "./version-C_ZRHikq.js";
5
- import { t as ASGProvider } from "./asg-provider-DoP-32eU.js";
4
+ import { t as getCdkdVersion } from "./version-CnQK6Jeh.js";
5
+ import { t as ASGProvider } from "./asg-provider-BocQJsjl.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";
@@ -20,15 +20,15 @@ import { CreateSecretCommand, DeleteSecretCommand, DescribeSecretCommand, GetSec
20
20
  import { AddTagsToResourceCommand, DeleteParameterCommand, DescribeParametersCommand, GetParameterCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$2, ParameterNotFound, PutParameterCommand, RemoveTagsFromResourceCommand, SSMClient } from "@aws-sdk/client-ssm";
21
21
  import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDistributionCommand, CreateDistributionWithTagsCommand, CreateOriginAccessControlCommand, DeleteCloudFrontOriginAccessIdentityCommand, DeleteDistributionCommand, DeleteOriginAccessControlCommand, DistributionAlreadyExists, GetCloudFrontOriginAccessIdentityCommand, GetDistributionCommand, GetDistributionConfigCommand, GetOriginAccessControlCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$3, NoSuchCloudFrontOriginAccessIdentity, NoSuchDistribution, NoSuchOriginAccessControl, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6, UpdateCloudFrontOriginAccessIdentityCommand, UpdateDistributionCommand, UpdateOriginAccessControlCommand } from "@aws-sdk/client-cloudfront";
22
22
  import { CloudWatchClient, DeleteAlarmsCommand, DeleteAnomalyDetectorCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutAnomalyDetectorCommand, PutMetricAlarmCommand, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch";
23
- import { AssociateKmsKeyCommand, CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, DisassociateKmsKeyCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$5, TagResourceCommand as TagResourceCommand$8, UntagResourceCommand as UntagResourceCommand$8 } from "@aws-sdk/client-cloudwatch-logs";
23
+ import { AssociateKmsKeyCommand, CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, DescribeLogStreamsCommand, DisassociateKmsKeyCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$5, TagResourceCommand as TagResourceCommand$8, UntagResourceCommand as UntagResourceCommand$8 } from "@aws-sdk/client-cloudwatch-logs";
24
24
  import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, CreateEvaluatorCommand, DeleteAgentRuntimeCommand, DeleteEvaluatorCommand, GetAgentRuntimeCommand, GetBrowserCommand, GetCodeInterpreterCommand, GetEvaluatorCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$6, ResourceNotFoundException as ResourceNotFoundException$6, TagResourceCommand as TagResourceCommand$9, UntagResourceCommand as UntagResourceCommand$9, UpdateAgentRuntimeCommand, UpdateEvaluatorCommand } from "@aws-sdk/client-bedrock-agentcore-control";
25
25
  import { AddTagsToResourceCommand as AddTagsToResourceCommand$1, CreateCacheClusterCommand, CreateCacheSubnetGroupCommand, DeleteCacheClusterCommand, DeleteCacheSubnetGroupCommand, DescribeCacheClustersCommand, DescribeCacheSubnetGroupsCommand, ElastiCacheClient, ListTagsForResourceCommand as ListTagsForResourceCommand$7, ModifyCacheClusterCommand, ModifyCacheSubnetGroupCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$1 } from "@aws-sdk/client-elasticache";
26
26
  import { ACMClient, AddTagsToCertificateCommand, DeleteCertificateCommand, DescribeCertificateCommand, ListTagsForCertificateCommand, RemoveTagsFromCertificateCommand, RequestCertificateCommand, ResourceNotFoundException as ResourceNotFoundException$7, UpdateCertificateOptionsCommand } from "@aws-sdk/client-acm";
27
27
  import { CreateMicrovmImageCommand, DeleteMicrovmImageCommand, GetMicrovmImageCommand, LambdaMicrovmsClient, ListTagsCommand as ListTagsCommand$1, ResourceNotFoundException as ResourceNotFoundException$8, TagResourceCommand as TagResourceCommand$10, UntagResourceCommand as UntagResourceCommand$10, UpdateMicrovmImageCommand } from "@aws-sdk/client-lambda-microvms";
28
28
  import * as fs from "node:fs";
29
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
29
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
30
30
  import * as path from "node:path";
31
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
31
+ import { basename, dirname, isAbsolute, resolve } from "node:path";
32
32
  import { execFile, spawn } from "node:child_process";
33
33
  import { tmpdir } from "node:os";
34
34
  import { AssociateVPCWithHostedZoneCommand, ChangeResourceRecordSetsCommand, ChangeTagsForResourceCommand, CreateHostedZoneCommand, CreateQueryLoggingConfigCommand, DeleteHostedZoneCommand, DeleteQueryLoggingConfigCommand, DisassociateVPCFromHostedZoneCommand, GetHostedZoneCommand, HostedZoneAlreadyExists, ListHostedZonesByNameCommand, ListQueryLoggingConfigsCommand, ListResourceRecordSetsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$8, Route53Client, UpdateHostedZoneCommentCommand, UpdateHostedZoneFeaturesCommand } from "@aws-sdk/client-route-53";
@@ -779,8 +779,8 @@ const recreateViaSdkProviderOption = new Option("--recreate-via-sdk-provider <lo
779
779
  * force would create a false sense of granularity (the user is opting
780
780
  * into a footgun; pretending to scope it per-resource is misleading).
781
781
  */
782
- const forceStatefulRecreationOption = new Option("--force-stateful-recreation", "Confirm a data-losing destroy + recreate of a stateful resource. Required whenever a replacement target is a stateful type databases, filesystems, KMS keys, table / vector storage, source repositories, log groups with retention, and more; docs/cli-deploy-safety.md carries the full list, and S3 is the special case (a non-empty bucket at pre-flight, ANY bucket mid-deploy, where the emptiness probe cannot run) on the --recreate-via-cc-api / --recreate-via-sdk-provider pre-flight, with --replace, AND on the paths a plain deploy reaches with no flag: a template immutable-property change, and an in-place update the provisioning layer rejects. Destroy + recreate loses ALL data in the resource — no automatic data migration. Full opt-in for CI use: --recreate-via-cc-api <id> --force-stateful-recreation --yes.").default(false);
783
- const replaceOption = new Option("--replace", "Replace (DELETE + CREATE) a resource whose in-place update is rejected because an immutable property changed and AWS exposes no update API for it (e.g. Lambda LayerVersion content, EFS AccessPoint, ECS TaskDefinition, ApiGatewayV2 immutable fields). Without this flag such a change fails the deploy. Stateful types (RDS / DynamoDB / EFS / S3 with data / Logs with retention / etc.) ALSO require --force-stateful-recreation since the replacement is a data-losing DELETE + CREATE.").default(false);
782
+ const forceStatefulRecreationOption = new Option("--force-stateful-recreation", "Confirm a data-losing destroy + recreate of a stateful resource. Required whenever a replacement target is a stateful type on the --recreate-via-cc-api / --recreate-via-sdk-provider pre-flight, with --replace, AND on the paths a plain deploy reaches with no flag: a template immutable-property change, and an in-place update the provisioning layer rejects. Stateful types are databases, filesystems, KMS keys, table / vector storage, source repositories, and more; docs/cli-deploy-safety.md carries the full list. S3 buckets and log groups are the conditional cases: at pre-flight, a non-empty bucket, or a log group with retention, with log streams, or whose emptiness the probe could not settle; mid-deploy, ANY bucket or log group, because the emptiness probe cannot run there. The two probes differ on failure the bucket fails open, the log group closed. Destroy + recreate loses ALL data in the resource — no automatic data migration. Full opt-in for CI use: --recreate-via-cc-api <id> --force-stateful-recreation --yes.").default(false);
783
+ const replaceOption = new Option("--replace", "Replace (DELETE + CREATE) a resource whose in-place update is rejected because an immutable property changed and AWS exposes no update API for it (e.g. Lambda LayerVersion content, EFS AccessPoint, ECS TaskDefinition, ApiGatewayV2 immutable fields). Without this flag such a change fails the deploy. Stateful types (RDS / DynamoDB / EFS / any S3 bucket / any Logs log group / etc.) ALSO require --force-stateful-recreation since the replacement is a data-losing DELETE + CREATE — mid-deploy neither emptiness probe can run, so both conditional types count as stateful there.").default(false);
784
784
  /**
785
785
  * Issue #1002 PR 2 — per-invocation legacy pin for asset destinations
786
786
  * (design §4.2). Shared by `deploy`, `diff`, `import`, and `publish-assets`
@@ -2495,12 +2495,15 @@ function isPromptAbortError(error) {
2495
2495
  * no-op for fresh deploys and should error out with a clear
2496
2496
  * message rather than silently apply.
2497
2497
  * 3. Stateful-resource guard: every named target whose resource type
2498
- * is in {@link STATEFUL_TYPES} (or conditionally stateful — S3
2499
- * bucket with objects, LogGroup with retention) MUST be matched
2500
- * by an explicit `--force-stateful-recreation` flag. The sync
2501
- * first-cut runs from the recorded properties alone; the live
2502
- * `s3:ListObjectVersions` probe (issue [#648]) promotes a `null`
2503
- * reason to `'has-objects'` when a bucket actually contains data.
2498
+ * is in {@link STATEFUL_TYPES} (or conditionally stateful — an S3
2499
+ * bucket holding objects, a LogGroup with retention or with log
2500
+ * streams) MUST be matched by an explicit
2501
+ * `--force-stateful-recreation` flag. The sync first-cut runs from
2502
+ * the recorded properties alone; the live probes promote a `null`
2503
+ * reason afterwards — `s3:ListObjectVersions` to `'has-objects'`
2504
+ * when a bucket actually contains data (issue [#648]), and
2505
+ * `logs:DescribeLogStreams` to `'has-log-events'` when a log group
2506
+ * is not provably empty (issue [#2558]).
2504
2507
  * 4. Multi-region refusal: every named target whose resource type
2505
2508
  * is in {@link MULTI_REGION_RECREATE_BLOCKED_TYPES} (e.g.
2506
2509
  * `AWS::DynamoDB::GlobalTable`) is refused outright. Out of
@@ -2516,11 +2519,12 @@ function isPromptAbortError(error) {
2516
2519
  /**
2517
2520
  * Plan-time validation of the user's recreate-via-cc-api list.
2518
2521
  *
2519
- * Pure with respect to AWS — does NOT probe S3 bucket emptiness. Wrap
2520
- * the result with {@link probeAndRevalidateStateful} to promote S3
2521
- * targets' `statefulReason` via a live `s3:ListObjectVersions` round-trip
2522
- * before rendering errors. The deploy command does this; the validator
2523
- * itself stays sync so unit tests don't need an S3 mock.
2522
+ * Pure with respect to AWS — does NOT probe S3 bucket emptiness, nor
2523
+ * log-group emptiness. Wrap the result with
2524
+ * {@link probeAndRevalidateStateful} to promote deferred targets'
2525
+ * `statefulReason` via a live round-trip before rendering errors. The
2526
+ * deploy command does this; the validator itself stays sync so unit tests
2527
+ * don't need AWS mocks.
2524
2528
  *
2525
2529
  * Input order is preserved; duplicate logical ids in the user's input
2526
2530
  * are deduplicated.
@@ -2681,63 +2685,176 @@ function renderRecreateTargetsErrors(validation) {
2681
2685
  return lines.length > 0 ? lines.join("\n") : null;
2682
2686
  }
2683
2687
  /**
2684
- * Async S3 object probe (issue [#648]).
2685
- *
2686
- * For every `AWS::S3::Bucket` target whose sync {@link StatefulReason}
2687
- * is `null` (the sync map defers — see {@link isStatefulRecreateTargetSync}),
2688
- * issues a single-page `ListObjectVersions(MaxKeys=1)` against the
2689
- * bucket's recorded physical id. When the bucket has at least one
2690
- * current object, prior version, OR delete-marker, promotes the
2691
- * target's `statefulReason` to `'has-objects'`.
2692
- *
2693
- * Uses `ListObjectVersions` rather than `ListObjectsV2` so the probe
2694
- * mirrors the s3-bucket-provider's `emptyBucket` view: a versioned
2695
- * bucket whose current keys have all been soft-deleted (so
2696
- * `ListObjectsV2.KeyCount === 0`) still holds prior versions +
2697
- * delete-markers that the destroy + recreate cycle would lose. Using
2698
- * the same listing API as the provider ensures the probe and the
2699
- * destroy path agree on "empty".
2700
- *
2701
- * **Soft-fail on probe errors**: if `ListObjectVersions` throws
2702
- * (permission denied, bucket-not-found mid-flight, transient network
2703
- * error), logs a warn and leaves the target's `statefulReason` at the
2704
- * sync result (`null`). The user can decide to proceed without the
2705
- * probe by passing `--force-stateful-recreation`.
2706
- *
2707
- * Returns a NEW array of targets; the input is not mutated. Non-S3
2708
- * targets and S3 targets whose sync reason is already non-null are
2709
- * passed through unchanged.
2710
- */
2711
- async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = getLogger().child("recreate-targets")) {
2688
+ * Trim-then-lower-case, the pair `CloudControlProvider` applies to both sides
2689
+ * of its own region assert. `canonicalizeRegion` only lower-cases, so a state
2690
+ * record carrying stray whitespace would still fail a `!==` compare.
2691
+ */
2692
+ function foldRegion(region) {
2693
+ return canonicalizeRegion(region?.trim());
2694
+ }
2695
+ /**
2696
+ * Async emptiness probes for the two conditionally stateful types
2697
+ * (issues [#648] / [#2558]).
2698
+ *
2699
+ * For every target whose sync {@link StatefulReason} is `null` which for
2700
+ * these two types means DEFER, not "not stateful" (see
2701
+ * {@link isStatefulRecreateTargetSync}) issues one single-page listing and
2702
+ * promotes the reason when the resource is not provably empty:
2703
+ *
2704
+ * - `AWS::S3::Bucket` → `ListObjectVersions(MaxKeys=1)` against the
2705
+ * bucket's recorded physical id, promoting to `'has-objects'` when the
2706
+ * bucket has at least one current object, prior version, OR
2707
+ * delete-marker. `ListObjectVersions` rather than `ListObjectsV2` so the
2708
+ * probe mirrors the s3-bucket-provider's `emptyBucket` view: a versioned
2709
+ * bucket whose current keys have all been soft-deleted (so
2710
+ * `ListObjectsV2.KeyCount === 0`) still holds prior versions +
2711
+ * delete-markers that the destroy + recreate cycle would lose. Using the
2712
+ * same listing API as the provider ensures the probe and the destroy path
2713
+ * agree on "empty".
2714
+ * - `AWS::Logs::LogGroup` → `DescribeLogStreams(limit=1)` against the log
2715
+ * group's recorded name. Only ONE response shape leaves the target
2716
+ * un-promoted: a PRESENT, zero-length `logStreams` with no `nextToken`.
2717
+ * A page with a stream, an ABSENT `logStreams` (the SDK types it
2718
+ * optional), and a zero-length page carrying a continuation token all
2719
+ * promote to `'has-log-events'` — the last two are non-answers, and an
2720
+ * unprovable emptiness must not read as empty. **Stream presence, not
2721
+ * `storedBytes`,** and the choice is load-bearing in both directions:
2722
+ * * Every log event belongs to a log stream, so ZERO streams is a
2723
+ * structural proof that the group holds no events — the only kind of
2724
+ * "empty" this guard may act on.
2725
+ * * `LogStream.storedBytes` cannot be used at all: the SDK still
2726
+ * declares the field, and the AWS API reference says "As of June 17,
2727
+ * 2019, this parameter is no longer supported for log streams, and is
2728
+ * always reported as zero" (quoted verbatim in
2729
+ * `@aws-sdk/client-cloudwatch-logs`'s own `LogStream.storedBytes`
2730
+ * JSDoc, which also marks it `@deprecated`). A probe reading it would
2731
+ * report EVERY log group empty.
2732
+ * * The same SDK note says the log GROUP's own `storedBytes` is "not
2733
+ * affected", so that field is not ruled out the way the stream's is —
2734
+ * but cdkd does not read it either, and the ground is not a claim
2735
+ * about how fresh it is (this repo has measured nothing about that).
2736
+ * Stream presence needs no size semantics at all: zero streams is a
2737
+ * STRUCTURAL proof, and it over-blocks rather than under-blocks (a
2738
+ * group holding only empty streams counts as non-empty), which is the
2739
+ * side this guard must err on.
2740
+ *
2741
+ * **Probe failures fail CLOSED for the log group, OPEN for the bucket**, and
2742
+ * the divergence is deliberate rather than an oversight. The bucket's
2743
+ * soft-fail is pre-existing shipped behaviour (issue [#648]) documented on
2744
+ * `docs/cli-deploy-safety.md`; the log group's arm is new with issue [#2558],
2745
+ * whose whole subject is that an unprovable emptiness must not read as empty.
2746
+ * So a failed `DescribeLogStreams` (permission denied, throttling) warns AND
2747
+ * promotes to `'has-log-events'`: the user gets a refusal naming the remedies,
2748
+ * not a silent recreate. The ONE carve-out is a typed
2749
+ * `ResourceNotFoundException`, which is an ANSWER rather than a failure to get
2750
+ * one — a log group AWS says does not exist provably holds no events.
2751
+ *
2752
+ * Returns a NEW array of targets; the input is not mutated. Targets of other
2753
+ * types, and targets whose sync reason is already non-null, are passed through
2754
+ * unchanged — including a `'has-retention'` log group, whose verdict the bag
2755
+ * already settled and no probe may weaken.
2756
+ */
2757
+ async function probeStatefulRecreateTargetsAsync(targets, clients, logger = getLogger().child("recreate-targets")) {
2712
2758
  const promoted = [];
2759
+ const probeRetryOptions = {
2760
+ maxRetries: 3,
2761
+ initialDelayMs: 500,
2762
+ logger,
2763
+ isRetryable: (_message, error) => isThrottlingError(error),
2764
+ ...clients.sleep && { sleep: clients.sleep }
2765
+ };
2713
2766
  for (const target of targets) {
2714
- if (target.resourceType !== "AWS::S3::Bucket" || target.statefulReason !== null) {
2767
+ if (target.statefulReason !== null) {
2715
2768
  promoted.push({ ...target });
2716
2769
  continue;
2717
2770
  }
2718
- try {
2719
- const result = await s3Client.send(new ListObjectVersionsCommand({
2720
- Bucket: target.physicalId,
2721
- MaxKeys: 1
2722
- }));
2723
- const hasVersions = (result.Versions?.length ?? 0) > 0;
2724
- const hasDeleteMarkers = (result.DeleteMarkers?.length ?? 0) > 0;
2725
- if (hasVersions || hasDeleteMarkers) promoted.push({
2726
- ...target,
2727
- statefulReason: "has-objects"
2728
- });
2729
- else promoted.push({ ...target });
2730
- } catch (e) {
2731
- logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: live S3 probe failed for ${target.logicalId} (bucket ${target.physicalId}); leaving stateful guard at the sync result. If the bucket might be non-empty, re-run with --force-stateful-recreation. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
2732
- promoted.push({ ...target });
2771
+ if (target.resourceType === "AWS::S3::Bucket") {
2772
+ try {
2773
+ const result = await withRetry(() => clients.s3.send(new ListObjectVersionsCommand({
2774
+ Bucket: target.physicalId,
2775
+ MaxKeys: 1
2776
+ })), target.logicalId, probeRetryOptions);
2777
+ const hasVersions = (result.Versions?.length ?? 0) > 0;
2778
+ const hasDeleteMarkers = (result.DeleteMarkers?.length ?? 0) > 0;
2779
+ const truncated = result.IsTruncated === true || !!result.NextKeyMarker || !!result.NextVersionIdMarker;
2780
+ if (hasVersions || hasDeleteMarkers) promoted.push({
2781
+ ...target,
2782
+ statefulReason: "has-objects"
2783
+ });
2784
+ else if (truncated) {
2785
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: S3 answered the emptiness probe for ${target.logicalId} (bucket ${target.physicalId}) without settling it (an empty page carrying a continuation marker); treating the bucket as NOT provably empty. Re-run to retry the probe, or — only if the bucket really is disposable — re-run with --force-stateful-recreation (that flag has NO per-resource granularity and clears the guard for every target in the run).`);
2786
+ promoted.push({
2787
+ ...target,
2788
+ statefulReason: "has-objects"
2789
+ });
2790
+ } else promoted.push({ ...target });
2791
+ } catch (e) {
2792
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: live S3 probe failed for ${target.logicalId} (bucket ${target.physicalId}); leaving stateful guard at the sync result. If the bucket might be non-empty, re-run with --force-stateful-recreation. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
2793
+ promoted.push({ ...target });
2794
+ }
2795
+ continue;
2733
2796
  }
2797
+ if (target.resourceType === "AWS::Logs::LogGroup") {
2798
+ try {
2799
+ const result = await withRetry(() => clients.cloudWatchLogs.send(new DescribeLogStreamsCommand({
2800
+ logGroupName: target.physicalId,
2801
+ limit: 1
2802
+ })), target.logicalId, probeRetryOptions);
2803
+ const streams = result.logStreams;
2804
+ if (streams !== void 0 && streams.length === 0 && !result.nextToken) promoted.push({ ...target });
2805
+ else if (streams !== void 0 && streams.length > 0) promoted.push({
2806
+ ...target,
2807
+ statefulReason: "has-log-events"
2808
+ });
2809
+ else {
2810
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: CloudWatch Logs answered the emptiness probe for ${target.logicalId} (log group ${target.physicalId}) without settling it (${streams === void 0 ? "no logStreams field in the response" : "an empty page carrying a continuation token"}); treating the log group as NOT provably empty. An unset or zero RetentionInDays is CloudWatch Logs' "never expire", so an unprovable emptiness must not read as empty. Re-run to retry the probe, or — only if the log group really is disposable — re-run with --force-stateful-recreation (that flag has NO per-resource granularity and clears the guard for every target in the run).`);
2811
+ promoted.push({
2812
+ ...target,
2813
+ statefulReason: "has-log-events"
2814
+ });
2815
+ }
2816
+ } catch (e) {
2817
+ if (e instanceof ResourceNotFoundException$5) {
2818
+ let regionVerified = true;
2819
+ const recordedRegion = foldRegion(clients.expectedRegion);
2820
+ if (recordedRegion) {
2821
+ let clientRegion;
2822
+ try {
2823
+ clientRegion = foldRegion(await clients.cloudWatchLogs.config.region());
2824
+ assertRegionMatch(clientRegion, recordedRegion, target.resourceType, target.logicalId, target.physicalId);
2825
+ } catch {
2826
+ regionVerified = false;
2827
+ const cause = clientRegion ? `the client region (${clientRegion}) does not match the region cdkd state records for it (${recordedRegion}) — a not-found from the wrong region says nothing about the log group. Fix the region mismatch, or` : `the CloudWatch Logs client's own region could not be resolved, so the not-found cannot be attributed to the region cdkd state records (${recordedRegion}). Fix the client's region configuration, or`;
2828
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: CloudWatch Logs reported ${target.logicalId} (log group ${target.physicalId}) missing, but ${cause} re-run with --force-stateful-recreation if the log group really is disposable (that flag clears the data guard for every target in the run). Until then it is treated as NOT provably empty.`);
2829
+ }
2830
+ }
2831
+ if (regionVerified) {
2832
+ logger.debug(`--recreate-via-cc-api / --recreate-via-sdk-provider: log group ${target.physicalId} (${target.logicalId}) does not exist, so it holds no events — not stateful.`);
2833
+ promoted.push({ ...target });
2834
+ continue;
2835
+ }
2836
+ promoted.push({
2837
+ ...target,
2838
+ statefulReason: "has-log-events"
2839
+ });
2840
+ continue;
2841
+ }
2842
+ logger.warn(`--recreate-via-cc-api / --recreate-via-sdk-provider: live CloudWatch Logs probe failed for ${target.logicalId} (log group ${target.physicalId}); treating the log group as NOT provably empty. An unset or zero RetentionInDays is CloudWatch Logs' "never expire", so an unprovable emptiness must not read as empty. Fixes, cheapest first: grant logs:DescribeLogStreams and re-run, or re-run as-is if this was transient (CloudWatch Logs throttles this API aggressively). Only if the log group really is disposable, re-run with --force-stateful-recreation — that flag has NO per-resource granularity and clears the guard for every target in the run. Underlying error: ${e instanceof Error ? e.message : String(e)}`);
2843
+ promoted.push({
2844
+ ...target,
2845
+ statefulReason: "has-log-events"
2846
+ });
2847
+ }
2848
+ continue;
2849
+ }
2850
+ promoted.push({ ...target });
2734
2851
  }
2735
2852
  return promoted;
2736
2853
  }
2737
2854
  /**
2738
2855
  * Async re-validation of the stateful-guard slice of a
2739
- * {@link RecreateTargetsValidation}, after promoting S3 bucket reasons
2740
- * via {@link probeStatefulRecreateTargetsAsync}.
2856
+ * {@link RecreateTargetsValidation}, after promoting the deferred S3 bucket
2857
+ * and log group reasons via {@link probeStatefulRecreateTargetsAsync}.
2741
2858
  *
2742
2859
  * Skips the probe entirely when `forceStatefulRecreation: true` — the
2743
2860
  * sync validation already omits the blocked list in that case, and
@@ -2750,7 +2867,7 @@ async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = get
2750
2867
  */
2751
2868
  async function probeAndRevalidateStateful(input) {
2752
2869
  if (input.forceStatefulRecreation) return input.validation;
2753
- const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.s3Client);
2870
+ const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.clients);
2754
2871
  const blockedStatefulTargets = promoted.filter((t) => t.statefulReason !== null);
2755
2872
  return {
2756
2873
  ...input.validation,
@@ -2853,11 +2970,27 @@ function renderDownstreamConsumers(producerStack, consumers) {
2853
2970
  * like a successful skipped-deploy.
2854
2971
  *
2855
2972
  * The per-target plan surfaces a **DATA LOSS** prefix for stateful
2856
- * targets (those with a non-null `statefulReason` after the live
2857
- * `s3:ListObjectVersions` probe see issue [#648]); these reached pre-flight
2858
- * only because the user opted in with `--force-stateful-recreation`,
2859
- * so the prompt's **DATA LOSS** wording is the third "stop and think"
2860
- * moment.
2973
+ * targets; a target reaches this prompt still carrying a stateful reason only
2974
+ * because the user opted in with `--force-stateful-recreation`, so the
2975
+ * **DATA LOSS** wording is the third "stop and think" moment.
2976
+ *
2977
+ * **The prefix is NOT read off `statefulReason` alone**, and the reason is the
2978
+ * flag that makes the prompt reachable in the first place (issue [#2558]'s
2979
+ * review round). `probeAndRevalidateStateful` returns EARLY under
2980
+ * `--force-stateful-recreation` — it never issues the live
2981
+ * `s3:ListObjectVersions` / `logs:DescribeLogStreams` probes — so under that
2982
+ * flag every target still carries the SYNC verdict, and for the two
2983
+ * CONDITIONAL types the sync verdict of a deferral is `null`. `null` there
2984
+ * does not mean "holds nothing": an `AWS::S3::Bucket` always defers, and an
2985
+ * `AWS::Logs::LogGroup` with no recorded retention is CloudWatch Logs'
2986
+ * NEVER EXPIRE. Rendering that as "not stateful" is precisely the reading
2987
+ * issue [#2558] exists to retire, and it was already wrong for a non-empty
2988
+ * bucket before that. So when the probe did not run, a `null` reason is
2989
+ * re-derived through {@link isStatefulRecreateTargetForReplace} — the
2990
+ * conservative mid-deploy predicate, a pure function of the type with no AWS
2991
+ * call, chosen over re-running the probe because re-running it under the force
2992
+ * flag would repopulate `blockedStatefulTargets` and turn the opt-in into a
2993
+ * refusal.
2861
2994
  */
2862
2995
  async function promptRecreateConfirm(input) {
2863
2996
  if (input.targets.length === 0) return true;
@@ -2869,10 +3002,13 @@ async function promptRecreateConfirm(input) {
2869
3002
  else if (toCcCount > 0) logger.warn(`--recreate-via-cc-api will destroy + recreate ${toCcCount} resource(s) via Cloud Control API on stack ${input.stackName}:`);
2870
3003
  else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
2871
3004
  for (const t of input.targets) {
2872
- const stateful = t.statefulReason !== null;
3005
+ const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0) : null;
3006
+ const rederived = rederivedReason !== null;
3007
+ const reason = t.statefulReason ?? rederivedReason;
3008
+ const stateful = reason !== null;
2873
3009
  const dataLossPrefix = stateful ? "**DATA LOSS** " : "";
2874
3010
  const directionTag = t.direction === "to-cc-api" ? " [SDK → CC]" : " [CC → SDK]";
2875
- const stateNote = stateful ? ` — stateful (${t.statefulReason}); --force-stateful-recreation acknowledged` : "";
3011
+ const stateNote = stateful ? ` — stateful (${rederived ? "emptiness not established — --force-stateful-recreation skips the probe" : renderStatefulReason(reason)}); --force-stateful-recreation acknowledged` : "";
2876
3012
  logger.warn(` - ${dataLossPrefix}${t.logicalId} (${t.resourceType})${directionTag}${stateNote}`);
2877
3013
  if (stateful) logger.warn(` DATA: all data in ${t.logicalId} will be lost (no automatic data migration)`);
2878
3014
  }
@@ -2950,12 +3086,13 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2950
3086
  * `printf 'y' |` (a real answer with no trailing newline) and `< /dev/null`
2951
3087
  * stay pending indefinitely.
2952
3088
  *
2953
- * REFUSE rather than auto-confirm, at every one of the nine sites this helper
2954
- * replaced. Every one of them guards a MUTATION — a rollback replay, a state
2955
- * record removal, an observed-property refresh, an orphan, an import, an
2956
- * export-then-delete-state, a drift accept/revert, a CloudFormation stack
2957
- * retirement, a state-bucket migration so silently answering "yes" on
2958
- * behalf of an absent operator is never the safe reading. `deploy.ts`'s
3089
+ * REFUSE rather than auto-confirm, at every one of the TEN sites that route
3090
+ * through this helper. Every one of them guards a MUTATION — a rollback
3091
+ * replay, a state record removal, an observed-property refresh, an orphan, an
3092
+ * import, an export-then-delete-state, a drift accept/revert, a
3093
+ * CloudFormation stack retirement, a state-bucket migration, an event-history
3094
+ * prune — so silently answering "yes" on behalf of an absent operator is never
3095
+ * the safe reading. `deploy.ts`'s
2959
3096
  * auto-confirm (`options.yes || !process.stdin.isTTY` -> proceed) stays the
2960
3097
  * deliberate exception, because a deploy that assumes "yes" is recoverable.
2961
3098
  *
@@ -2974,10 +3111,12 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2974
3111
  * `--yes` refuse instead of proceed, which is the inverse bug.
2975
3112
  *
2976
3113
  * Throws {@link CdkdError} with the `NON_INTERACTIVE_CONFIRM` code, matching
2977
- * `gc.ts` and `bootstrap-destroy.ts` — the only two of the five originally
2978
- * guarded prompts that carry a code (the other three throw a bare `Error` or
2979
- * a `LocalMigrateError`). `handleError` maps it to exit 1, so CI can branch on
2980
- * it.
3114
+ * `gc.ts` and `bootstrap-destroy.ts` — the only two of the four originally
3115
+ * guarded prompts that carry a code (the other two throw a bare `Error`).
3116
+ * `handleError` maps it to exit 1, so CI can branch on it.
3117
+ *
3118
+ * There were FIVE until `cdkd migrate` was removed (issue #2572); its prompt
3119
+ * threw a `LocalMigrateError`, and that class went with it.
2981
3120
  *
2982
3121
  * TWO GUARDED PROMPTS DELIBERATELY DO NOT ROUTE THROUGH HERE:
2983
3122
  * `destroy-runner.ts`'s per-stack prompt (the issue #2259 fix) and
@@ -2988,10 +3127,10 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2988
3127
  * passes an abort `signal` for the issue #2117 Ctrl-C handling. Folding them
2989
3128
  * in would also drag `destroy-runner.ts` (in the `integ-destroy` AND
2990
3129
  * `integ-broad` gate scopes) into a pure refactor's blast radius, buying a
2991
- * real-AWS run for no behaviour change. The other five pre-existing guarded
3130
+ * real-AWS run for no behaviour change. The other four pre-existing guarded
2992
3131
  * prompts (`gc.ts`, `bootstrap-destroy.ts`, `recreate-confirm-prompt.ts`,
2993
- * `prefix-migration-check.ts`, `migrate-command.ts`) stay put for the same
2994
- * reason: each guards its own flow with its own error type.
3132
+ * `prefix-migration-check.ts`) stay put for the same reason: each guards its
3133
+ * own flow with its own error type.
2995
3134
  *
2996
3135
  * @param prompt the question, WITHOUT its trailing `[y/N]` marker
2997
3136
  * @returns `true` only for `y` / `yes` (any case, trimmed)
@@ -21731,9 +21870,8 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
21731
21870
  * The CFn schema forbids `ProvisionedThroughput` / `OnDemandThroughput` /
21732
21871
  * `*ThroughputOverride` on `AWS::DynamoDB::GlobalTable`, so this only fires
21733
21872
  * for pre-#1387 cdkd state and hand-authored templates — but those ARE a
21734
- * first-class cdkd surface (`cdkd import --migrate-from-cloudformation`,
21735
- * `cdkd migrate --from-cfn-stack`), and the pre-fix code forwarded such a
21736
- * block VERBATIM. Three defects followed, and fixing any subset leaves a
21873
+ * first-class cdkd surface (`cdkd import --migrate-from-cloudformation`), and
21874
+ * the pre-fix code forwarded such a block VERBATIM. Three defects followed, and fixing any subset leaves a
21737
21875
  * live one, which is why this helper does all three at once:
21738
21876
  *
21739
21877
  * 1. **Coercion.** The verbatim forward was the ONE path that skipped
@@ -22937,7 +23075,7 @@ var LogsLogGroupProvider = class {
22937
23075
  const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
22938
23076
  const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
22939
23077
  const nextClass = normalizeClass(properties["LogGroupClass"]);
22940
- if (prevClass !== nextClass) throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. Re-deploy with ${properties["RetentionInDays"] ? "--replace --force-stateful-recreation" : "--replace"} to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change`);
23078
+ if (prevClass !== nextClass) throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. Re-deploy with --replace --force-stateful-recreation to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change. Note --force-stateful-recreation has NO per-resource granularity: it clears the data guard for every replacement target in the run`);
22941
23079
  const newKmsKeyId = properties["KmsKeyId"];
22942
23080
  if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
22943
23081
  if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
@@ -47289,9 +47427,32 @@ function isAccountIdPseudoParameter(value) {
47289
47427
  * [#1675](https://github.com/go-to-k/cdkd/issues/1675)).
47290
47428
  *
47291
47429
  * This is {@link importableString} plus a NUMBER arm, and the number arm is
47292
- * load-bearing rather than defensive: `cdkd import --migrate-from-cloudformation`
47293
- * reads the stack's ORIGINAL template (`cfn-stack-prefetch.ts`), where an
47294
- * unquoted YAML `CatalogId: 123456789012` parses as a JSON number. Rejecting it
47430
+ * load-bearing rather than defensive, and the reason is NOT the one this
47431
+ * comment gave for three revisions. It claimed
47432
+ * `cdkd import --migrate-from-cloudformation` (earlier: `cdkd migrate`) read
47433
+ * the source stack's ORIGINAL CloudFormation template and handed those raw
47434
+ * properties to providers. It never did. Both commands take properties from
47435
+ * the SYNTH assembly; `cdkd migrate` reached `runImport` with
47436
+ * `app: <assemblyDir>` plus a logical-id mapping, and the ORIGINAL template it
47437
+ * fetched was consumed only by transform detection and by the Pass-2
47438
+ * property-equality MATCH, never forwarded. The `GetTemplate` that survives in
47439
+ * `retire-cfn-stack.ts` feeds Retain injection and the re-upload, and reaches
47440
+ * no properties bag either. Do not restore any of that.
47441
+ *
47442
+ * The two producers that ARE real, both current:
47443
+ *
47444
+ * 1. A numeric literal in the synth template itself — `new CfnResource({
47445
+ * properties: { CatalogId: 123456789012 } })`, or an
47446
+ * `addPropertyOverride`. CDK emits the number verbatim; nothing between
47447
+ * synth and this provider stringifies it.
47448
+ * 2. The macro-expanded deploy template. `src/synthesis/macro-expander.ts`
47449
+ * fetches `GetTemplate` at `TemplateStage: 'Processed'` and parses the
47450
+ * JSON body as the template deploy / diff / import then use, so a macro
47451
+ * that emits an unquoted number lands here as a number.
47452
+ *
47453
+ * The DELETE path reaches it too, from the other side: `destroy-runner.ts`
47454
+ * hands the recorded `resource.properties` straight to `provider.delete`, so a
47455
+ * state row holding either shape above replays it at teardown. Rejecting it
47295
47456
  * as "not a string" would drop the field and silently retarget the call at the
47296
47457
  * account's DEFAULT catalog — which on a DELETE can destroy a same-named
47297
47458
  * resource that happens to live there. `Number.isFinite` keeps `NaN` /
@@ -49041,9 +49202,12 @@ const TARGET_DATABASE_MEMBERS = [
49041
49202
  const FEDERATED_DATABASE_MEMBERS = ["ConnectionName", "Identifier"];
49042
49203
  /**
49043
49204
  * Is this leaf value one cdkd can put on the wire? A STRING or a NUMBER — CFn
49044
- * is stringly typed and an unquoted YAML account id arrives as a number (the
49045
- * reason `catalogIdForApi` exists) and nothing else, so an unresolved
49046
- * intrinsic cannot ride an unchecked cast into the request.
49205
+ * is stringly typed, and an account id can reach a provider as a JSON number
49206
+ * (the reason {@link catalogIdForApi} exists; that function's note carries the
49207
+ * two producers, neither of which is a YAML read — an earlier version of this
49208
+ * sentence said "an unquoted YAML account id" and was wrong the same way) —
49209
+ * and nothing else, so an unresolved intrinsic cannot ride an unchecked cast
49210
+ * into the request.
49047
49211
  */
49048
49212
  const isSendableLeaf = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
49049
49213
  /**
@@ -62468,7 +62632,11 @@ async function deployCommand(stacks, options) {
62468
62632
  forceStatefulRecreation: options.forceStatefulRecreation ?? false,
62469
62633
  hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
62470
62634
  }),
62471
- s3Client: stackAwsClients.s3,
62635
+ clients: {
62636
+ s3: stackAwsClients.s3,
62637
+ cloudWatchLogs: stackAwsClients.cloudWatchLogs,
62638
+ expectedRegion: stateForRecreateCheck?.state.region
62639
+ },
62472
62640
  forceStatefulRecreation: options.forceStatefulRecreation ?? false
62473
62641
  });
62474
62642
  const errorBlock = renderRecreateTargetsErrors(validation);
@@ -62486,6 +62654,7 @@ async function deployCommand(stacks, options) {
62486
62654
  stackName: stackInfo.stackName,
62487
62655
  targets: validation.targets,
62488
62656
  yes: options.yes ?? false,
62657
+ forceStatefulRecreation: options.forceStatefulRecreation ?? false,
62489
62658
  downstreamConsumers
62490
62659
  })) return;
62491
62660
  }
@@ -63040,10 +63209,11 @@ function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /*
63040
63209
  * unhealthy state — callers gate AWS-side mutations behind this set so
63041
63210
  * the user can settle the source before paying for further work.
63042
63211
  *
63043
- * Single source of truth — consumed by both `cdkd migrate`'s pre-flight
63044
- * check (`src/cli/commands/migrate/cfn-stack-prefetch.ts`) and the
63045
- * `cdkd import --migrate-from-cloudformation` retirement flow
63046
- * (`src/cli/commands/retire-cfn-stack.ts`).
63212
+ * Consumed by the `cdkd import --migrate-from-cloudformation` retirement
63213
+ * flow (`src/cli/commands/retire-cfn-stack.ts`). It had a second consumer,
63214
+ * `cdkd migrate`'s pre-flight check, until that command was removed (issue
63215
+ * #2572); the module stays separate so the next CloudFormation-reading
63216
+ * surface shares the set instead of re-spelling it.
63047
63217
  */
63048
63218
  const STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
63049
63219
  "CREATE_COMPLETE",
@@ -63857,20 +64027,20 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
63857
64027
  }
63858
64028
  /**
63859
64029
  * The CloudFormation-stack retirement confirmation prompt, reached from
63860
- * `cdkd import --migrate-from-cloudformation` and `cdkd migrate
63861
- * --retire-cfn-stack` (both spell the skip flag `-y` / `--yes`). Its only call
63862
- * site is inside the `if (!yes)` block above, which is what keeps
64030
+ * `cdkd import --migrate-from-cloudformation` (skip flag `-y` / `--yes`). Its
64031
+ * only call site is inside the `if (!yes)` block above, which is what keeps
63863
64032
  * `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing on a
63864
64033
  * `--yes` run.
63865
64034
  *
63866
- * TWO commands reach it, so the refusal NAMES BOTH. Every other site names one
63867
- * command because it has one; a refusal that named neither would leave a CI
63868
- * user knowing a flag is missing without knowing which invocation to put it
63869
- * on, and `confirmOrRefuse`'s own contract is that the refusal is the only
63870
- * thing that user sees.
64035
+ * The refusal NAMES that command, as every other site does, because
64036
+ * `confirmOrRefuse`'s contract is that the refusal is the only thing a CI user
64037
+ * sees: it has to say which invocation the missing flag belongs on. It named
64038
+ * TWO commands until `cdkd migrate --retire-cfn-stack` was removed with the
64039
+ * command itself (issue #2572); `cdkd import --migrate-from-cloudformation` is
64040
+ * now the sole way to reach this prompt.
63871
64041
  *
63872
64042
  * It also states that cdkd state has ALREADY been written. This prompt is the
63873
- * only one of the nine that fires AFTER its command's state write, so a
64043
+ * only one of the ten that fires AFTER its command's state write, so a
63874
64044
  * refusal here leaves a genuine split brain — cdkd claims the resources while
63875
64045
  * the CloudFormation stack is still live — and the operator has to know that
63876
64046
  * before deciding whether to re-run or finish by hand.
@@ -63878,7 +64048,7 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
63878
64048
  * Exported for unit testing — internal to the command flow otherwise.
63879
64049
  */
63880
64050
  async function confirmPrompt$6(prompt) {
63881
- 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." });
64051
+ return confirmOrRefuse(prompt, { refusal: "The CloudFormation stack retirement confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes on cdkd import --migrate-from-cloudformation. Alternatively, 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." });
63882
64052
  }
63883
64053
  /**
63884
64054
  * Flat `logicalId -> physicalId` map for ONE CloudFormation stack, or `null`
@@ -78732,6 +78902,7 @@ async function forceUnlockCommand(stackArgs, options) {
78732
78902
  setAwsClients(awsClients);
78733
78903
  const region = namedCliRegion(options.region) ?? "us-east-1";
78734
78904
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
78905
+ const failures = [];
78735
78906
  try {
78736
78907
  const stateConfig = {
78737
78908
  bucket: stateBucket,
@@ -78756,13 +78927,17 @@ async function forceUnlockCommand(stackArgs, options) {
78756
78927
  } catch (error) {
78757
78928
  const message = error instanceof Error ? error.message : String(error);
78758
78929
  if (message.includes("No lock found") || message.includes("NoSuchKey")) logger.info(`No lock found for stack: ${where}`);
78759
- else logger.error(`Failed to unlock stack ${where}: ${message}`);
78930
+ else {
78931
+ logger.error(`Failed to unlock stack ${where}: ${message}`);
78932
+ failures.push(`${where}: ${message}`);
78933
+ }
78760
78934
  }
78761
78935
  }
78762
78936
  }
78763
78937
  } finally {
78764
78938
  awsClients.destroy();
78765
78939
  }
78940
+ if (failures.length > 0) throw new CdkdError(`Failed to release ${failures.length} lock(s):\n` + failures.map((f) => ` - ${f}`).join("\n"), "FORCE_UNLOCK_FAILED");
78766
78941
  }
78767
78942
  /**
78768
78943
  * Create force-unlock command
@@ -78780,9 +78955,6 @@ function createForceUnlockCommand() {
78780
78955
 
78781
78956
  //#endregion
78782
78957
  //#region src/cli/commands/import.ts
78783
- async function runImport(stackArg, options) {
78784
- return importCommand(stackArg, options);
78785
- }
78786
78958
  async function importCommand(stackArg, options) {
78787
78959
  const logger = getLogger();
78788
78960
  if (options.verbose) {
@@ -79667,7 +79839,7 @@ function validateNestedStackShape(template, tree, parentStackName, nestedTemplat
79667
79839
  * **parent-first acquire, leaves-first release** (each level's
79668
79840
  * `finally` releases its child lock before sibling iteration
79669
79841
  * continues, and the root lock is the outermost — held by
79670
- * `runImport`'s `try` / `finally`). This is the conventional
79842
+ * `importCommand`'s `try` / `finally`). This is the conventional
79671
79843
  * hierarchical lock pattern: parent-first acquire prevents a
79672
79844
  * second cdkd import from racing past the root lock; leaves-first
79673
79845
  * release on success/failure means a mid-walk error never strands
@@ -81890,7 +82062,7 @@ async function applyCrossStackResolverToTask(task, context) {
81890
82062
 
81891
82063
  //#endregion
81892
82064
  //#region src/local/docker-runner.ts
81893
- const execFileAsync$4 = promisify(execFile);
82065
+ const execFileAsync$3 = promisify(execFile);
81894
82066
  /**
81895
82067
  * Wraps `docker pull` / `docker run` / `docker rm` for `cdkd local invoke`.
81896
82068
  *
@@ -81994,7 +82166,7 @@ async function runDetached(opts) {
81994
82166
  logger.debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
81995
82167
  if (collisions.length > 0) logger.warn(`Env var(s) ${collisions.map((k) => JSON.stringify(k)).join(", ")} share a name with a docker-client environment variable or have a malformed name (empty, or containing '=' / NUL), and were NOT passed to the container at all (a colliding name would hijack the docker client; a malformed name cannot form a valid environment variable). Rename the env var if the container needs it.`);
81996
82168
  try {
81997
- const { stdout } = await execFileAsync$4(getDockerCmd(), args, {
82169
+ const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
81998
82170
  maxBuffer: 10 * 1024 * 1024,
81999
82171
  env: dockerSpawnEnvWithSensitive(passthroughEnv)
82000
82172
  });
@@ -82034,7 +82206,7 @@ async function removeContainer(containerId) {
82034
82206
  if (!containerId) return;
82035
82207
  const logger = getLogger().child("docker");
82036
82208
  try {
82037
- await execFileAsync$4(getDockerCmd(), [
82209
+ await execFileAsync$3(getDockerCmd(), [
82038
82210
  "rm",
82039
82211
  "-f",
82040
82212
  containerId
@@ -82053,7 +82225,7 @@ async function removeContainer(containerId) {
82053
82225
  async function ensureDockerAvailable() {
82054
82226
  const cmd = getDockerCmd();
82055
82227
  try {
82056
- await execFileAsync$4(cmd, [
82228
+ await execFileAsync$3(cmd, [
82057
82229
  "version",
82058
82230
  "--format",
82059
82231
  "{{.Server.Version}}"
@@ -85067,7 +85239,7 @@ function createLocalStartApiCommand() {
85067
85239
 
85068
85240
  //#endregion
85069
85241
  //#region src/local/ecs-network.ts
85070
- const execFileAsync$3 = promisify(execFile);
85242
+ const execFileAsync$2 = promisify(execFile);
85071
85243
  /**
85072
85244
  * Docker network + AWS-published metadata-endpoints sidecar lifecycle for
85073
85245
  * `cdkd local run-task`. The sidecar (a small Go binary maintained by
@@ -85120,7 +85292,7 @@ async function createNetworkAndSidecar(args) {
85120
85292
  await pullImage(METADATA_ENDPOINT_IMAGE, skipPull);
85121
85293
  logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
85122
85294
  try {
85123
- await execFileAsync$3(getDockerCmd(), [
85295
+ await execFileAsync$2(getDockerCmd(), [
85124
85296
  "network",
85125
85297
  "create",
85126
85298
  "--driver",
@@ -85157,7 +85329,7 @@ async function createNetworkAndSidecar(args) {
85157
85329
  sidecarArgs.push(METADATA_ENDPOINT_IMAGE);
85158
85330
  logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
85159
85331
  try {
85160
- const { stdout } = await execFileAsync$3(getDockerCmd(), sidecarArgs, {
85332
+ const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, {
85161
85333
  maxBuffer: 10 * 1024 * 1024,
85162
85334
  env: dockerSpawnEnvWithSensitive(sidecarSensitiveEnv)
85163
85335
  });
@@ -85228,7 +85400,7 @@ async function destroyNetworkOnly(networkName) {
85228
85400
  if (!networkName) return;
85229
85401
  const logger = getLogger().child("ecs-network");
85230
85402
  try {
85231
- await execFileAsync$3(getDockerCmd(), [
85403
+ await execFileAsync$2(getDockerCmd(), [
85232
85404
  "network",
85233
85405
  "rm",
85234
85406
  networkName
@@ -85368,7 +85540,7 @@ async function resolveSsm(entry, shape, client) {
85368
85540
 
85369
85541
  //#endregion
85370
85542
  //#region src/local/ecs-task-runner.ts
85371
- const execFileAsync$2 = promisify(execFile);
85543
+ const execFileAsync$1 = promisify(execFile);
85372
85544
  /**
85373
85545
  * Top-level orchestrator for `cdkd local run-task`. Coordinates image
85374
85546
  * preparation, secret resolution, docker-network bring-up, container
@@ -85428,7 +85600,7 @@ async function cleanupEcsRun(state, options) {
85428
85600
  if (state.network && !state.network.ownedByCaller) await destroyTaskNetwork(state.network);
85429
85601
  state.network = void 0;
85430
85602
  for (const v of state.dockerVolumeNames) try {
85431
- await execFileAsync$2(getDockerCmd(), [
85603
+ await execFileAsync$1(getDockerCmd(), [
85432
85604
  "volume",
85433
85605
  "rm",
85434
85606
  v
@@ -85523,7 +85695,7 @@ async function runEcsTask(task, options, state) {
85523
85695
  let id;
85524
85696
  try {
85525
85697
  const sensitiveEnv = dockerEnvs.get(container.name);
85526
- const { stdout } = await execFileAsync$2(getDockerCmd(), args, {
85698
+ const { stdout } = await execFileAsync$1(getDockerCmd(), args, {
85527
85699
  maxBuffer: 10 * 1024 * 1024,
85528
85700
  env: dockerSpawnEnvWithSensitive(sensitiveEnv)
85529
85701
  });
@@ -85635,7 +85807,7 @@ async function waitForContainerHealthy(containerId, displayName) {
85635
85807
  let lastStatus = "";
85636
85808
  while (Date.now() < deadline) {
85637
85809
  try {
85638
- const { stdout } = await execFileAsync$2(getDockerCmd(), [
85810
+ const { stdout } = await execFileAsync$1(getDockerCmd(), [
85639
85811
  "inspect",
85640
85812
  "--format",
85641
85813
  "{{.State.Health.Status}}",
@@ -85658,7 +85830,7 @@ async function waitForContainerHealthy(containerId, displayName) {
85658
85830
  }
85659
85831
  async function waitForContainerExit(containerId) {
85660
85832
  try {
85661
- const { stdout } = await execFileAsync$2(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
85833
+ const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
85662
85834
  const code = Number.parseInt(stdout.trim(), 10);
85663
85835
  return Number.isFinite(code) ? code : 1;
85664
85836
  } catch (err) {
@@ -85668,7 +85840,7 @@ async function waitForContainerExit(containerId) {
85668
85840
  }
85669
85841
  async function stopContainer(containerId, graceSeconds) {
85670
85842
  try {
85671
- await execFileAsync$2(getDockerCmd(), [
85843
+ await execFileAsync$1(getDockerCmd(), [
85672
85844
  "stop",
85673
85845
  "-t",
85674
85846
  String(graceSeconds),
@@ -85793,7 +85965,7 @@ async function realizeDockerVolumes(volumes, state) {
85793
85965
  const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
85794
85966
  args.push(dockerVolumeName);
85795
85967
  try {
85796
- await execFileAsync$2(getDockerCmd(), args);
85968
+ await execFileAsync$1(getDockerCmd(), args);
85797
85969
  state.dockerVolumeNames.push(dockerVolumeName);
85798
85970
  logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
85799
85971
  } catch (err) {
@@ -86374,7 +86546,7 @@ function createLocalStartServiceCommand() {
86374
86546
  * after each reload (cdkd-specific — cdk-local treats single-shot as a
86375
86547
  * no-op WARN).
86376
86548
  */
86377
- const execFileAsync$1 = promisify(execFile);
86549
+ const execFileAsync = promisify(execFile);
86378
86550
  /**
86379
86551
  * Run the `--watch` reload loop. Returns when the invocation closes naturally
86380
86552
  * with no pending reload (one-shot finished, or `--ws` agent closed), or when
@@ -86525,7 +86697,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86525
86697
  const dockerCmd = getDockerCmd();
86526
86698
  let workdir;
86527
86699
  try {
86528
- const { stdout } = await execFileAsync$1(dockerCmd, [
86700
+ const { stdout } = await execFileAsync(dockerCmd, [
86529
86701
  "inspect",
86530
86702
  "--format",
86531
86703
  "{{.Config.WorkingDir}}",
@@ -86538,7 +86710,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86538
86710
  const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
86539
86711
  logger.info(`Soft-reload: docker cp ${newAssetSourceDir} -> ${containerId}:${workdirDest}; restart.`);
86540
86712
  try {
86541
- await execFileAsync$1(dockerCmd, [
86713
+ await execFileAsync(dockerCmd, [
86542
86714
  "cp",
86543
86715
  `${newAssetSourceDir}/.`,
86544
86716
  `${containerId}:${workdirDest}`
@@ -86547,7 +86719,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86547
86719
  throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
86548
86720
  }
86549
86721
  try {
86550
- await execFileAsync$1(dockerCmd, ["restart", containerId]);
86722
+ await execFileAsync(dockerCmd, ["restart", containerId]);
86551
86723
  } catch (err) {
86552
86724
  throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
86553
86725
  }
@@ -88733,1076 +88905,6 @@ function createLocalCommand() {
88733
88905
  return local;
88734
88906
  }
88735
88907
 
88736
- //#endregion
88737
- //#region src/cli/commands/migrate/cdk-cli-check.ts
88738
- const execFileAsync = promisify(execFile);
88739
- /**
88740
- * Minimum aws-cdk CLI version where `cdk migrate --from-stack` is
88741
- * considered stable (per the design doc at docs/design/465-cfn-migrate.md
88742
- * §4 — "Version requirement"). Below this we WARN but do not block,
88743
- * because users may have a working older release and a hard rejection
88744
- * would be more disruptive than a warning.
88745
- */
88746
- const RECOMMENDED_MIN_VERSION = "2.124.0";
88747
- /**
88748
- * Verify that the upstream `cdk` CLI is available on PATH (or at the
88749
- * override path passed via `--cdk-bin`) and report its version.
88750
- *
88751
- * Hard-errors with {@link MissingCdkCliError} when `cdk` cannot be
88752
- * spawned (ENOENT, permission denied, etc.). Emits a `warn` field on
88753
- * the result when the version string is below the recommended minimum
88754
- * — the caller decides how to surface it.
88755
- *
88756
- * `cdk --version` output format empirically (verified 2026-05-22
88757
- * against cdk@2.1112.0): `<MAJOR.MINOR.PATCH> (build <id>)`. The
88758
- * version is the first whitespace-separated token; the trailing
88759
- * `(build <id>)` is informational and ignored.
88760
- *
88761
- * @param cdkBinPath - Path to the `cdk` binary. Defaults to `'cdk'`
88762
- * which uses the system PATH for resolution.
88763
- */
88764
- async function verifyCdkCliAvailable(cdkBinPath = "cdk") {
88765
- let stdout;
88766
- try {
88767
- stdout = (await execFileAsync(cdkBinPath, ["--version"])).stdout ?? "";
88768
- } catch (err) {
88769
- throw new MissingCdkCliError(`Failed to run '${cdkBinPath} --version': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
88770
- }
88771
- const version = parseCdkVersion(stdout);
88772
- if (!version) throw new MissingCdkCliError(`'${cdkBinPath} --version' produced unexpected output: ${JSON.stringify(stdout.trim())}`);
88773
- if (compareSemver(version, RECOMMENDED_MIN_VERSION) < 0) return {
88774
- version,
88775
- warn: `cdk CLI version ${version} is older than the recommended minimum ${RECOMMENDED_MIN_VERSION}. 'cdkd migrate' relies on the stabilized 'cdk migrate --from-stack' flag; upgrade with 'npm install -g aws-cdk@latest' if you hit codegen issues.`
88776
- };
88777
- return { version };
88778
- }
88779
- /**
88780
- * Parse the first semver-shaped token (`<MAJOR>.<MINOR>.<PATCH>`) out of
88781
- * `cdk --version` stdout. Tolerates a trailing `(build <id>)` suffix.
88782
- *
88783
- * Returns `undefined` when no semver-shaped token is present so the
88784
- * caller can surface the raw stdout in the error message.
88785
- */
88786
- function parseCdkVersion(stdout) {
88787
- const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
88788
- if (!match) return void 0;
88789
- return `${match[1]}.${match[2]}.${match[3]}`;
88790
- }
88791
- /**
88792
- * Compare two semver strings (`MAJOR.MINOR.PATCH`). Returns a negative
88793
- * number when `a < b`, zero when `a === b`, positive when `a > b`.
88794
- *
88795
- * Strictly numeric comparison per component — no pre-release / build
88796
- * suffix handling needed for this use case (`cdk --version` always
88797
- * emits a stable `X.Y.Z` triple).
88798
- */
88799
- function compareSemver(a, b) {
88800
- const aParts = a.split(".").map((n) => Number.parseInt(n, 10));
88801
- const bParts = b.split(".").map((n) => Number.parseInt(n, 10));
88802
- for (let i = 0; i < 3; i++) {
88803
- const aN = aParts[i] ?? 0;
88804
- const bN = bParts[i] ?? 0;
88805
- if (aN !== bN) return aN - bN;
88806
- }
88807
- return 0;
88808
- }
88809
-
88810
- //#endregion
88811
- //#region src/cli/commands/migrate/cfn-stack-prefetch.ts
88812
- /**
88813
- * Resource types that `cdkd migrate` refuses to adopt under any
88814
- * circumstances. The rationale per type:
88815
- *
88816
- * - `AWS::CloudFormation::CustomResource` — Lambda-backed Custom
88817
- * Resources whose response protocol (cfn-response over a
88818
- * pre-signed S3 URL) is incompatible with cdkd's import flow. The
88819
- * backing Lambda's onCreate handler would need to be re-invoked
88820
- * for a cdkd-side adoption to make sense, and that's structurally
88821
- * different from the metadata-transfer migration this command
88822
- * provides.
88823
- *
88824
- * - `AWS::CloudFormation::Stack` — nested stacks. cdkd's recursive
88825
- * `NestedStackProvider` handles fresh `cdkd deploy` (#459), but
88826
- * `cdk migrate` flattens nested stacks into separate generated apps
88827
- * in a way that doesn't round-trip cleanly into cdkd's parent~child
88828
- * state-key layout, so the migrate command keeps rejecting them.
88829
- * Adopting an existing CFn-managed nested-stack hierarchy is
88830
- * deferred to issue
88831
- * [#464](https://github.com/go-to-k/cdkd/issues/464).
88832
- *
88833
- * - `Custom::*` — any user-defined Custom Resource type prefix.
88834
- * Same rationale as `AWS::CloudFormation::CustomResource`.
88835
- */
88836
- const HARD_REJECT_RESOURCE_TYPES = /* @__PURE__ */ new Set(["AWS::CloudFormation::CustomResource", "AWS::CloudFormation::Stack"]);
88837
- /**
88838
- * Fetch the source CFn stack's current state + resources + transform
88839
- * info. Issues 3 read-only AWS API calls (`DescribeStacks`,
88840
- * `DescribeStackResources`, `GetTemplate(Stage=Original)`); none of
88841
- * them mutate AWS state. The result is fed into
88842
- * {@link validatePrefetchResult} for the hard-reject check and
88843
- * surfaced to {@link runMigrateLibrary}'s caller (PR B) as part of
88844
- * `RunMigrateLibraryResult`.
88845
- *
88846
- * `DescribeStackResources` is preferred over `ListStackResources`
88847
- * because the response is unpaginated up to 500 resources (CFn's hard
88848
- * stack cap) — no pagination loop needed for the migration use case.
88849
- */
88850
- async function prefetchCfnStack(stackName, cfnClient) {
88851
- const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }))).Stacks?.[0];
88852
- if (!stack) throw new LocalMigrateError(`CloudFormation stack '${stackName}' not found.`);
88853
- const stackStatus = stack.StackStatus ?? "";
88854
- const resourcesResp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: stackName }));
88855
- const resources = [];
88856
- for (const r of resourcesResp.StackResources ?? []) {
88857
- if (!r.LogicalResourceId || !r.PhysicalResourceId || !r.ResourceType) continue;
88858
- resources.push({
88859
- LogicalResourceId: r.LogicalResourceId,
88860
- PhysicalResourceId: r.PhysicalResourceId,
88861
- ResourceType: r.ResourceType
88862
- });
88863
- }
88864
- let transformInfo = {
88865
- hasSamTransform: false,
88866
- hasIncludeTransform: false
88867
- };
88868
- let sourceCfnTemplate = null;
88869
- try {
88870
- const tplResp = await cfnClient.send(new GetTemplateCommand({
88871
- StackName: stackName,
88872
- TemplateStage: "Original"
88873
- }));
88874
- if (tplResp.TemplateBody) {
88875
- try {
88876
- sourceCfnTemplate = parseCfnTemplate(tplResp.TemplateBody);
88877
- } catch {
88878
- sourceCfnTemplate = null;
88879
- }
88880
- transformInfo = detectTransformsFromParsed(sourceCfnTemplate);
88881
- }
88882
- } catch (err) {
88883
- const detail = err instanceof Error ? err.message : String(err);
88884
- getLogger().warn(`[migrate] GetTemplate failed for '${stackName}': ${detail}. Skipping transform detection.`);
88885
- }
88886
- return {
88887
- stackStatus,
88888
- resources,
88889
- transformInfo,
88890
- sourceCfnTemplate
88891
- };
88892
- }
88893
- /**
88894
- * Pre-flight reject when the source CFn stack contains any
88895
- * un-migratable resource type OR is in a non-terminal state. Surfaces
88896
- * SAM / Include transforms via the result's `transformInfo` instead —
88897
- * the caller is responsible for emitting INFO logs.
88898
- *
88899
- * Per #465 Q2 (parent-session decision): every hard-reject case
88900
- * routes the user to the standalone `cdk migrate --from-stack <name>
88901
- * --output-dir ./tmp` flow so they can manually inspect the generated
88902
- * code and decide how to proceed (e.g. delete the Custom Resource
88903
- * from the source CFn stack before re-running cdkd migrate).
88904
- */
88905
- function validatePrefetchResult(result) {
88906
- if (!STABLE_TERMINAL_STATUSES.has(result.stackStatus)) throw new LocalMigrateError(`CloudFormation stack is in status '${result.stackStatus}', which is not a stable terminal state. Wait for the stack to settle (or roll back) before running 'cdkd migrate'.`);
88907
- const offenders = [];
88908
- for (const r of result.resources) if (isHardRejectType(r.ResourceType)) offenders.push({
88909
- LogicalResourceId: r.LogicalResourceId,
88910
- ResourceType: r.ResourceType
88911
- });
88912
- if (offenders.length === 0) return;
88913
- throw new LocalMigrateError(`Source CloudFormation stack contains resource types that 'cdkd migrate' cannot adopt:\n${offenders.map((o) => ` - ${o.LogicalResourceId} (${o.ResourceType})`).join("\n")}\n\nLambda-backed Custom Resources and nested CloudFormation stacks are out of scope for #465.\nTo migrate the rest of the stack, either:\n 1. Delete the unsupported resources from the source CFn stack first, OR\n 2. Run upstream 'cdk migrate' standalone to inspect the generated code:\n cdk migrate --from-stack --stack-name <source-stack> --output-path ./tmp\n then hand-author CDK constructs to replace the Custom Resource semantics.`);
88914
- }
88915
- /**
88916
- * Whether the given resource type is in the hard-reject set. Matches
88917
- * literal types from {@link HARD_REJECT_RESOURCE_TYPES} AND the
88918
- * `Custom::*` prefix pattern (user-defined Custom Resource types).
88919
- */
88920
- function isHardRejectType(resourceType) {
88921
- if (HARD_REJECT_RESOURCE_TYPES.has(resourceType)) return true;
88922
- if (resourceType.startsWith("Custom::")) return true;
88923
- return false;
88924
- }
88925
- /**
88926
- * Scan an already-parsed CFn template object for stack-level
88927
- * Transform blocks. Accepts both the scalar form (`Transform:
88928
- * AWS::Serverless-2016-10-31`) and the array form (`Transform:
88929
- * [AWS::Serverless-2016-10-31, ...]`).
88930
- *
88931
- * The caller is responsible for parsing the raw template body via the
88932
- * CFn-aware codec; this helper operates on the parsed object so the
88933
- * `GetTemplate` response can be parsed once and reused.
88934
- */
88935
- function detectTransformsFromParsed(parsed) {
88936
- if (!parsed || typeof parsed !== "object") return {
88937
- hasSamTransform: false,
88938
- hasIncludeTransform: false
88939
- };
88940
- const transformRaw = parsed["Transform"];
88941
- const transforms = Array.isArray(transformRaw) ? transformRaw.map((t) => String(t)) : typeof transformRaw === "string" ? [transformRaw] : [];
88942
- return {
88943
- hasSamTransform: transforms.some((t) => t.startsWith("AWS::Serverless")),
88944
- hasIncludeTransform: transforms.some((t) => t === "AWS::Include" || t.startsWith("AWS::Include"))
88945
- };
88946
- }
88947
-
88948
- //#endregion
88949
- //#region src/cli/commands/migrate/output-dir-guard.ts
88950
- /**
88951
- * Refuse to run `cdkd migrate` when the upstream `cdk migrate` would
88952
- * write its output into an existing non-empty directory. `cdk migrate`
88953
- * itself silently overwrites in that case; cdkd surfaces the collision
88954
- * up-front with the exact recovery command so the user is not left
88955
- * with a half-overwritten CDK app.
88956
- *
88957
- * Behavior matrix (`outputPath = <user-supplied dir>`, `stackName =
88958
- * <CFn stack name>`; cdk writes to `<outputPath>/<stackName>`):
88959
- * - target dir does not exist → OK
88960
- * - target dir exists but is empty → OK (treated as fresh)
88961
- * - target path is a file, not a directory → typed error
88962
- * - target dir exists and contains entries → typed error with
88963
- * the canonical
88964
- * `rm -rf` recovery
88965
- * command in the
88966
- * message
88967
- *
88968
- * The parent `outputPath` is NOT checked — `cdk migrate` creates
88969
- * `<outputPath>` itself if missing, so a non-existent parent is fine.
88970
- */
88971
- function assertOutputDirAvailable(outputPath, stackName) {
88972
- const targetDir = resolve(outputPath, stackName);
88973
- if (!existsSync(targetDir)) return;
88974
- if (!statSync(targetDir).isDirectory()) throw new LocalMigrateError(`Output path '${targetDir}' exists but is not a directory. Remove it (or pass --output-dir <NEW_PATH>) and retry:\n rm -f ${targetDir} && cdkd migrate --from-cfn-stack ${stackName}`);
88975
- const entries = readdirSync(targetDir);
88976
- if (entries.length === 0) return;
88977
- throw new LocalMigrateError(`Output dir '${targetDir}' already exists and is non-empty (e.g. '${join(targetDir, entries[0])}'); remove it or pass --output-dir <NEW_PATH>:\n rm -rf ${targetDir} && cdkd migrate --from-cfn-stack ${stackName}`);
88978
- }
88979
-
88980
- //#endregion
88981
- //#region src/cli/commands/migrate/cdk-migrate-spawn.ts
88982
- /**
88983
- * Spawn `cdk migrate --from-stack` as a subprocess and stream its
88984
- * output through cdkd's logger.
88985
- *
88986
- * Why subprocess (not Node module import): the upstream `cdk` CLI is
88987
- * a bundled CommonJS app with no stable public API; pinning a
88988
- * runtime-injected `aws-cdk-lib` from cdkd is impractical. Subprocess
88989
- * isolation also gives us a clean failure boundary — non-zero exit
88990
- * surfaces as {@link LocalMigrateError} with the captured streams
88991
- * folded into the error message.
88992
- *
88993
- * Streaming is preferred over a buffered `execFile` because `cdk
88994
- * migrate` emits progress lines (downloading the resource schema,
88995
- * synthesizing the L1 code for each resource) that users expect to
88996
- * see in real time. Captured streams are still returned for caller
88997
- * inspection.
88998
- */
88999
- async function spawnCdkMigrate(opts) {
89000
- const logger = getLogger();
89001
- const cdkBin = opts.cdkBinPath ?? "cdk";
89002
- const language = opts.language ?? "typescript";
89003
- const outputDir = resolve(opts.outputPath, opts.stackName);
89004
- const args = [
89005
- "migrate",
89006
- "--from-stack",
89007
- "--stack-name",
89008
- opts.fromStackName,
89009
- "--output-path",
89010
- opts.outputPath,
89011
- "--language",
89012
- language
89013
- ];
89014
- if (opts.region) args.push("--region", opts.region);
89015
- if (opts.account) args.push("--account", opts.account);
89016
- if (opts.profile) args.push("--profile", opts.profile);
89017
- for (const filter of opts.filters ?? []) args.push("--filter", filter);
89018
- const env = {
89019
- ...process.env,
89020
- ...opts.extraEnv ?? {}
89021
- };
89022
- logger.info(`[cdk-migrate] ${cdkBin} ${args.join(" ")}`);
89023
- return await new Promise((resolvePromise, rejectPromise) => {
89024
- let stdout = "";
89025
- let stderr = "";
89026
- let child;
89027
- try {
89028
- child = spawn(cdkBin, args, {
89029
- env,
89030
- stdio: [
89031
- "ignore",
89032
- "pipe",
89033
- "pipe"
89034
- ]
89035
- });
89036
- } catch (err) {
89037
- const detail = err instanceof Error ? err.message : String(err);
89038
- rejectPromise(new LocalMigrateError(`Failed to spawn '${cdkBin} migrate': ${detail}`, err instanceof Error ? err : void 0));
89039
- return;
89040
- }
89041
- child.stdout?.on("data", (chunk) => {
89042
- const text = chunk.toString("utf-8");
89043
- stdout += text;
89044
- const trimmed = text.replace(/\n$/, "");
89045
- if (trimmed) logger.info(`[cdk-migrate] ${trimmed}`);
89046
- });
89047
- child.stderr?.on("data", (chunk) => {
89048
- const text = chunk.toString("utf-8");
89049
- stderr += text;
89050
- const trimmed = text.replace(/\n$/, "");
89051
- if (trimmed) logger.warn(`[cdk-migrate] ${trimmed}`);
89052
- });
89053
- child.on("error", (err) => {
89054
- rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' subprocess error: ${err.message}`, err));
89055
- });
89056
- child.on("close", (code, signal) => {
89057
- if (code === 0) {
89058
- resolvePromise({
89059
- outputDir,
89060
- stdout,
89061
- stderr
89062
- });
89063
- return;
89064
- }
89065
- const exitDetail = signal !== null ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "process closed without a code or signal";
89066
- rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' ${exitDetail}.\n--- stdout ---\n${stdout || "(empty)"}\n--- stderr ---\n${stderr || "(empty)"}\n`));
89067
- });
89068
- });
89069
- }
89070
-
89071
- //#endregion
89072
- //#region src/cli/commands/migrate/synth-after-migrate.ts
89073
- /**
89074
- * Run `npm install` inside the generated CDK app directory so the
89075
- * `cdk synth` step (which dynamically loads `aws-cdk-lib`) has its
89076
- * dependencies in place.
89077
- *
89078
- * Skippable via `skipInstall: true` — CI users with a pre-populated
89079
- * node_modules cache can save the ~30s `npm install` round-trip.
89080
- *
89081
- * Streams stdout / stderr through cdkd's logger so users see progress
89082
- * for the often-slow npm step. Non-zero exit surfaces as a typed
89083
- * {@link LocalMigrateError} with the captured streams folded into
89084
- * the error message.
89085
- */
89086
- async function installGeneratedAppDeps(outputDir, opts) {
89087
- const logger = getLogger();
89088
- if (opts.skipInstall) {
89089
- logger.info(`[cdk-migrate] Skipping 'npm install' (--skip-install).`);
89090
- return;
89091
- }
89092
- const absDir = resolve(outputDir);
89093
- if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'npm install'.`);
89094
- logger.info(`[cdk-migrate] Running 'npm install' in ${absDir}...`);
89095
- await runStreamingCommand("npm", ["install"], absDir, opts.extraEnv ? {
89096
- ...process.env,
89097
- ...opts.extraEnv
89098
- } : void 0, "npm install");
89099
- }
89100
- /**
89101
- * Run `cdk synth --quiet` inside the generated CDK app directory and
89102
- * return both the assembly dir path and the parsed root-stack
89103
- * template.
89104
- *
89105
- * `--quiet` suppresses the noisy template echo that `cdk synth`
89106
- * defaults to on stdout — we read the template from disk
89107
- * (`cdk.out/<StackName>.template.json`) instead.
89108
- *
89109
- * Skippable via `skipSynth: true` — useful when PR B's orchestrator
89110
- * needs the codegen output without running synth (e.g. preview /
89111
- * dry-run modes).
89112
- */
89113
- async function synthGeneratedApp(outputDir, opts) {
89114
- const logger = getLogger();
89115
- const absDir = resolve(outputDir);
89116
- const assemblyDir = join(absDir, "cdk.out");
89117
- if (opts.skipSynth) {
89118
- logger.info(`[cdk-migrate] Skipping 'cdk synth' (--skip-synth).`);
89119
- return {
89120
- assemblyDir,
89121
- templateBody: null
89122
- };
89123
- }
89124
- if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'cdk synth'.`);
89125
- const cdkBin = opts.cdkBinPath ?? "cdk";
89126
- const env = {
89127
- ...process.env,
89128
- ...opts.extraEnv ?? {}
89129
- };
89130
- logger.info(`[cdk-migrate] Running '${cdkBin} synth --quiet' in ${absDir}...`);
89131
- await runStreamingCommand(cdkBin, ["synth", "--quiet"], absDir, env, `${cdkBin} synth`);
89132
- if (!existsSync(assemblyDir)) throw new LocalMigrateError(`'cdk synth' completed but produced no '${assemblyDir}' directory.`);
89133
- const templates = readdirSync(assemblyDir).filter((f) => f.endsWith(".template.json"));
89134
- if (templates.length === 0) return {
89135
- assemblyDir,
89136
- templateBody: null
89137
- };
89138
- const templatePath = join(assemblyDir, templates[0]);
89139
- let templateBody;
89140
- try {
89141
- const raw = readFileSync(templatePath, "utf-8");
89142
- templateBody = JSON.parse(raw);
89143
- } catch (err) {
89144
- throw new LocalMigrateError(`Failed to parse generated template '${templatePath}': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
89145
- }
89146
- return {
89147
- assemblyDir,
89148
- templateBody
89149
- };
89150
- }
89151
- /**
89152
- * Helper — spawn a subprocess in the given working directory, stream
89153
- * its output through cdkd's logger, and reject with a typed
89154
- * {@link LocalMigrateError} on non-zero exit.
89155
- *
89156
- * Factored out so the `npm install` and `cdk synth` call sites use the
89157
- * same shape; both expose progress to the user and surface failures
89158
- * with the captured streams.
89159
- */
89160
- async function runStreamingCommand(bin, args, cwd, env, label) {
89161
- const logger = getLogger();
89162
- return await new Promise((resolvePromise, rejectPromise) => {
89163
- let stdout = "";
89164
- let stderr = "";
89165
- let child;
89166
- try {
89167
- child = spawn(bin, args, {
89168
- cwd,
89169
- env: env ?? process.env,
89170
- stdio: [
89171
- "ignore",
89172
- "pipe",
89173
- "pipe"
89174
- ]
89175
- });
89176
- } catch (err) {
89177
- rejectPromise(new LocalMigrateError(`Failed to spawn ${label}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
89178
- return;
89179
- }
89180
- child.stdout?.on("data", (chunk) => {
89181
- const text = chunk.toString("utf-8");
89182
- stdout += text;
89183
- const trimmed = text.replace(/\n$/, "");
89184
- if (trimmed) logger.info(`[${label}] ${trimmed}`);
89185
- });
89186
- child.stderr?.on("data", (chunk) => {
89187
- const text = chunk.toString("utf-8");
89188
- stderr += text;
89189
- const trimmed = text.replace(/\n$/, "");
89190
- if (trimmed) logger.warn(`[${label}] ${trimmed}`);
89191
- });
89192
- child.on("error", (err) => {
89193
- rejectPromise(new LocalMigrateError(`${label} subprocess error: ${err.message}`, err));
89194
- });
89195
- child.on("close", (code, signal) => {
89196
- if (code === 0) {
89197
- resolvePromise();
89198
- return;
89199
- }
89200
- rejectPromise(new LocalMigrateError(`${label} ${signal !== null ? `killed by signal ${signal}` : code !== null ? `exited with code ${code}` : "process closed without a code or signal"}.\n--- stdout ---\n${stdout || "(empty)"}\n--- stderr ---\n${stderr || "(empty)"}\n`));
89201
- });
89202
- });
89203
- }
89204
-
89205
- //#endregion
89206
- //#region src/cli/commands/migrate/index.ts
89207
- /**
89208
- * Build an extra-env bag for subprocesses (`npm install`, `cdk synth`)
89209
- * so they inherit the AWS profile the caller selected via `--profile`.
89210
- *
89211
- * Without this, `cdk synth` falls back to the SDK default credential
89212
- * chain (or no credentials at all) when the user runs cdkd under a
89213
- * non-default profile — which silently produces a template synthesized
89214
- * against the wrong account / region context.
89215
- */
89216
- function buildAwsEnv(opts) {
89217
- return opts.profile ? { AWS_PROFILE: opts.profile } : {};
89218
- }
89219
- /**
89220
- * PR A library entry point — orchestrates the codegen + synth half of
89221
- * `cdkd migrate --from-cfn-stack` without writing cdkd state, running
89222
- * import, or touching the source CFn stack.
89223
- *
89224
- * Flow:
89225
- * 1. `verifyCdkCliAvailable` — hard-fail if `cdk` is missing.
89226
- * 2. `prefetchCfnStack` — read source stack state + resources +
89227
- * transform info.
89228
- * 3. `validatePrefetchResult` — reject CR / nested-stack / non-
89229
- * terminal state; INFO log SAM / Include transforms.
89230
- * 4. `assertOutputDirAvailable` — refuse pre-existing non-empty
89231
- * output dir.
89232
- * 5. `spawnCdkMigrate` — run `cdk migrate --from-stack` subprocess.
89233
- * 6. `installGeneratedAppDeps` (gated by `skipInstall`).
89234
- * 7. `synthGeneratedApp` (gated by `skipSynth`).
89235
- * 8. Return every artifact PR B will consume.
89236
- *
89237
- * Per #465 Q4 (parent-session decision): PR A is library-only. The CLI
89238
- * command (`cdkd migrate`), state writes, retire flow, and resource-
89239
- * mapping algorithm all live in PR B.
89240
- */
89241
- async function runMigrateLibrary(opts) {
89242
- const logger = getLogger();
89243
- const stackName = opts.fromCfnStack;
89244
- const outputPath = opts.outputDir ?? resolve(process.cwd(), stackName);
89245
- logger.info(`[migrate] Verifying upstream 'cdk' CLI...`);
89246
- const cliCheck = await verifyCdkCliAvailable(opts.cdkBinPath);
89247
- logger.info(`[migrate] Using cdk CLI v${cliCheck.version}`);
89248
- if (cliCheck.warn) logger.warn(`[migrate] ${cliCheck.warn}`);
89249
- logger.info(`[migrate] Pre-fetching CloudFormation stack '${stackName}'...`);
89250
- const cfnClient = buildCfnClient(opts);
89251
- let prefetch;
89252
- try {
89253
- prefetch = await prefetchCfnStack(stackName, cfnClient);
89254
- } finally {
89255
- cfnClient.destroy?.();
89256
- }
89257
- validatePrefetchResult(prefetch);
89258
- if (prefetch.transformInfo.hasSamTransform) logger.info("[migrate] INFO: source CFn stack uses 'AWS::Serverless' transform; cdk migrate will expand it client-side and the generated CDK code will use plain Lambda + API Gateway L1 constructs (not SAM).");
89259
- if (prefetch.transformInfo.hasIncludeTransform) logger.info("[migrate] INFO: source CFn stack uses 'AWS::Include' transform; cdk migrate will expand it client-side and the generated CDK code will inline the included template content.");
89260
- assertOutputDirAvailable(outputPath, stackName);
89261
- const spawnResult = await spawnCdkMigrate({
89262
- stackName,
89263
- fromStackName: stackName,
89264
- outputPath,
89265
- ...opts.language && { language: opts.language },
89266
- ...opts.region && { region: opts.region },
89267
- ...opts.account && { account: opts.account },
89268
- ...opts.filters && { filters: opts.filters },
89269
- ...opts.profile && { profile: opts.profile },
89270
- ...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath }
89271
- });
89272
- await installGeneratedAppDeps(spawnResult.outputDir, {
89273
- skipInstall: opts.skipInstall ?? false,
89274
- extraEnv: buildAwsEnv(opts)
89275
- });
89276
- const synthResult = await synthGeneratedApp(spawnResult.outputDir, {
89277
- skipSynth: opts.skipSynth ?? false,
89278
- ...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath },
89279
- extraEnv: buildAwsEnv(opts)
89280
- });
89281
- if (prefetch.sourceCfnTemplate === null) throw new LocalMigrateError(`Could not read the source CloudFormation template for '${stackName}' (GetTemplate failed during pre-flight). Re-run after granting cloudformation:GetTemplate on the stack, or check the earlier warning for details.`);
89282
- return {
89283
- outputDir: spawnResult.outputDir,
89284
- assemblyDir: synthResult.assemblyDir,
89285
- templateBody: synthResult.templateBody,
89286
- sourceCfnTemplate: prefetch.sourceCfnTemplate,
89287
- sourceResources: prefetch.resources
89288
- };
89289
- }
89290
- /**
89291
- * Build a CloudFormation client using cdkd's shared {@link AwsClients}
89292
- * factory so the credential chain matches every other cdkd command.
89293
- *
89294
- * Keeping the construction in one place makes the per-command CFn-
89295
- * client recipe a single readable hook for future plumbing (e.g.
89296
- * threading PR B's `--role-arn` STS-assume).
89297
- */
89298
- function buildCfnClient(opts) {
89299
- const config = {};
89300
- if (opts.region) config.region = opts.region;
89301
- if (opts.profile) config.profile = opts.profile;
89302
- return new AwsClients(config).getCloudFormationClient();
89303
- }
89304
-
89305
- //#endregion
89306
- //#region src/cli/commands/migrate/resource-mapper.ts
89307
- /**
89308
- * Run the 2-pass mapping algorithm against `(sourceCfnTemplate,
89309
- * synthTemplate, sourceResources)` and return the resolved mapping plus
89310
- * any unmatched entries.
89311
- *
89312
- * Pure-functional: no AWS calls, no I/O, no mutation of inputs. Safe to
89313
- * call repeatedly in tests against shared fixtures.
89314
- *
89315
- * Throws when an override references a synth id that does not exist —
89316
- * this is a user error in the hand-edited mapping JSON and surfacing
89317
- * the available synth ids in the message is the path back to a working
89318
- * config.
89319
- */
89320
- function buildResourceMapping(opts) {
89321
- const sourceEntries = extractSourceResources(opts.sourceCfnTemplate);
89322
- const synthEntries = extractSynthResources(opts.synthTemplate);
89323
- const physicalIdByLogicalId = /* @__PURE__ */ new Map();
89324
- for (const r of opts.sourceResources) physicalIdByLogicalId.set(r.LogicalResourceId, r.PhysicalResourceId);
89325
- const overrides = opts.overrides ?? {};
89326
- const synthLogicalIds = new Set(synthEntries.map((s) => s.logicalId));
89327
- for (const [srcId, synthId] of Object.entries(overrides)) if (!synthLogicalIds.has(synthId)) throw new Error(`Resource-mapping override targets synth logical id '${synthId}' (for source id '${srcId}'), but that id is not in the synthesized template. Available synth ids: ${[...synthLogicalIds].sort().join(", ")}`);
89328
- const missingPhysicalIds = [];
89329
- for (const src of sourceEntries) if (!physicalIdByLogicalId.has(src.logicalId)) missingPhysicalIds.push(` - ${src.logicalId} (${src.type})`);
89330
- if (missingPhysicalIds.length > 0) throw new Error(`Source CFn resource(s) not returned by DescribeStackResources — cannot migrate:\n${missingPhysicalIds.join("\n")}\n\nThe stack may have been mid-operation; wait for it to settle (CREATE_COMPLETE / UPDATE_COMPLETE / etc.) and retry.`);
89331
- const synthByLastPathSegment = indexSynthByLastPathSegment(synthEntries);
89332
- const synthByLogicalId = new Map(synthEntries.map((s) => [s.logicalId, s]));
89333
- const claimedSynthIds = /* @__PURE__ */ new Set();
89334
- const pairs = [];
89335
- const unmatched = [];
89336
- const sourcesHandledByOverride = /* @__PURE__ */ new Set();
89337
- for (const [srcId, synthId] of Object.entries(overrides)) {
89338
- const src = sourceEntries.find((e) => e.logicalId === srcId);
89339
- if (!src) {
89340
- const availableSrc = sourceEntries.map((e) => e.logicalId).sort();
89341
- throw new Error(`Resource-mapping override references source logical id '${srcId}', but no resource with that id exists in the source CloudFormation template. Available source ids: ${availableSrc.join(", ")}`);
89342
- }
89343
- const synth = synthByLogicalId.get(synthId);
89344
- if (!synth) continue;
89345
- const physicalId = physicalIdByLogicalId.get(src.logicalId);
89346
- pairs.push({
89347
- sourceLogicalId: src.logicalId,
89348
- synthLogicalId: synth.logicalId,
89349
- physicalId,
89350
- resourceType: src.type
89351
- });
89352
- claimedSynthIds.add(synth.logicalId);
89353
- sourcesHandledByOverride.add(src.logicalId);
89354
- }
89355
- const passOnePending = [];
89356
- for (const src of sourceEntries) {
89357
- if (sourcesHandledByOverride.has(src.logicalId)) continue;
89358
- const candidatesForLastSegment = synthByLastPathSegment.get(src.logicalId) ?? [];
89359
- const rawCandidateCount = candidatesForLastSegment.length;
89360
- const availableCandidates = candidatesForLastSegment.filter((c) => !claimedSynthIds.has(c.logicalId));
89361
- if (rawCandidateCount === 1 && availableCandidates.length === 1) {
89362
- const synth = availableCandidates[0];
89363
- pairs.push({
89364
- sourceLogicalId: src.logicalId,
89365
- synthLogicalId: synth.logicalId,
89366
- physicalId: physicalIdByLogicalId.get(src.logicalId),
89367
- resourceType: src.type
89368
- });
89369
- claimedSynthIds.add(synth.logicalId);
89370
- } else passOnePending.push(src);
89371
- }
89372
- for (const src of passOnePending) {
89373
- const candidatesSameType = synthEntries.filter((s) => s.type === src.type && !claimedSynthIds.has(s.logicalId));
89374
- const propertyMatches = candidatesSameType.filter((s) => deepEqualIgnoreNoValue(src.properties, s.properties));
89375
- if (propertyMatches.length === 1) {
89376
- const synth = propertyMatches[0];
89377
- pairs.push({
89378
- sourceLogicalId: src.logicalId,
89379
- synthLogicalId: synth.logicalId,
89380
- physicalId: physicalIdByLogicalId.get(src.logicalId),
89381
- resourceType: src.type
89382
- });
89383
- claimedSynthIds.add(synth.logicalId);
89384
- } else {
89385
- const isCollision = (synthByLastPathSegment.get(src.logicalId) ?? []).length >= 2;
89386
- unmatched.push({
89387
- sourceLogicalId: src.logicalId,
89388
- resourceType: src.type,
89389
- candidates: candidatesSameType.map((c) => c.logicalId).sort(),
89390
- reason: isCollision ? "logical-id-collision" : "no-match"
89391
- });
89392
- }
89393
- }
89394
- const mapping = {};
89395
- for (const p of pairs) mapping[p.sourceLogicalId] = p.synthLogicalId;
89396
- return {
89397
- mapping,
89398
- pairs,
89399
- unmatched
89400
- };
89401
- }
89402
- /**
89403
- * Walk a parsed CFn template's `Resources` and return one entry per
89404
- * resource. `Type` defaults to `''` and `Properties` to `{}` when the
89405
- * template omits them (CFn allows resources with only `DeletionPolicy`
89406
- * + `Type`); the mapper's downstream deep-equal handles `{}` cleanly.
89407
- *
89408
- * Top-level keys other than `Resources` (Conditions / Parameters /
89409
- * Rules / Outputs / Mappings / Metadata) are NOT walked — the mapping
89410
- * is resource-to-resource only, and §5.5 of the design doc spells out
89411
- * the four CDK-synth injections that live in those sibling keys.
89412
- */
89413
- function extractSourceResources(template) {
89414
- if (!template || typeof template !== "object") return [];
89415
- const resources = template["Resources"];
89416
- if (!resources || typeof resources !== "object") return [];
89417
- const out = [];
89418
- for (const [logicalId, raw] of Object.entries(resources)) {
89419
- if (!raw || typeof raw !== "object") continue;
89420
- const r = raw;
89421
- const type = typeof r["Type"] === "string" ? r["Type"] : "";
89422
- const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
89423
- out.push({
89424
- logicalId,
89425
- type,
89426
- properties: props
89427
- });
89428
- }
89429
- return out;
89430
- }
89431
- /**
89432
- * Walk the synth template's `Resources` and return one entry per
89433
- * resource, EXCLUDING `AWS::CDK::Metadata` (synth-only sentinel that has
89434
- * no source counterpart). Captures `Metadata['aws:cdk:path']` so Pass 1
89435
- * can match against the last `/`-separated segment.
89436
- *
89437
- * The four other CDK-synth injections (CDKMetadataAvailable Condition,
89438
- * BootstrapVersion Parameter, CheckBootstrapVersion Rule) live outside
89439
- * `Resources` and are structurally excluded by this function.
89440
- */
89441
- function extractSynthResources(template) {
89442
- if (!template || typeof template !== "object") return [];
89443
- const resources = template["Resources"];
89444
- if (!resources || typeof resources !== "object") return [];
89445
- const out = [];
89446
- for (const [logicalId, raw] of Object.entries(resources)) {
89447
- if (!raw || typeof raw !== "object") continue;
89448
- const r = raw;
89449
- const type = typeof r["Type"] === "string" ? r["Type"] : "";
89450
- if (type === "AWS::CDK::Metadata") continue;
89451
- const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
89452
- const meta = r["Metadata"] && typeof r["Metadata"] === "object" ? r["Metadata"] : {};
89453
- const path = typeof meta["aws:cdk:path"] === "string" ? meta["aws:cdk:path"] : "";
89454
- out.push({
89455
- logicalId,
89456
- type,
89457
- properties: props,
89458
- awsCdkPath: path
89459
- });
89460
- }
89461
- return out;
89462
- }
89463
- /**
89464
- * Group synth entries by the LAST `/`-separated segment of their
89465
- * `aws:cdk:path` metadata so Pass 1 can look up the source logical id
89466
- * directly. Per [docs/design/465-cfn-migrate.md](../../../../docs/design/465-cfn-migrate.md) §5.5:
89467
- * `cdk migrate` emits `<StackName>/<LogicalId>` as the metadata value,
89468
- * so the last segment IS the source logical id in the typical case.
89469
- *
89470
- * Entries without an `aws:cdk:path` are silently skipped — they cannot
89471
- * be addressed by source logical id and will only be reachable via
89472
- * Pass 2's Properties match.
89473
- */
89474
- function indexSynthByLastPathSegment(entries) {
89475
- const out = /* @__PURE__ */ new Map();
89476
- for (const e of entries) {
89477
- if (!e.awsCdkPath) continue;
89478
- const lastSegment = e.awsCdkPath.split("/").pop() ?? "";
89479
- if (!lastSegment) continue;
89480
- const bucket = out.get(lastSegment);
89481
- if (bucket) bucket.push(e);
89482
- else out.set(lastSegment, [e]);
89483
- }
89484
- return out;
89485
- }
89486
- /**
89487
- * Recursive deep equality between two values, with two CFn-specific
89488
- * accommodations:
89489
- *
89490
- * 1. Object key order is NOT significant — `JSON.stringify` would
89491
- * diverge on the canonical-source-vs-synth-alphabetized case
89492
- * (verified by PR A's empirical run, §5.5 point 3) so we walk the
89493
- * keys explicitly.
89494
- * 2. `AWS::NoValue` placeholder values are stripped before compare —
89495
- * `cdk migrate`'s codegen sometimes emits the placeholder for a
89496
- * property the source template simply omitted, and the structural
89497
- * intent matches. Source side never has the placeholder; synth side
89498
- * can on either side of a nested object.
89499
- *
89500
- * Arrays are compared positionally (order IS significant — Tags arrays
89501
- * and similar carry ordering semantics).
89502
- */
89503
- function deepEqualIgnoreNoValue(a, b) {
89504
- if (a === b) return true;
89505
- if (isAwsNoValue(a) !== isAwsNoValue(b)) return false;
89506
- if (isAwsNoValue(a) && isAwsNoValue(b)) return true;
89507
- if (typeof a !== typeof b) return false;
89508
- if (a === null || b === null) return a === b;
89509
- if (typeof a !== "object") return a === b;
89510
- if (Array.isArray(a) !== Array.isArray(b)) return false;
89511
- if (Array.isArray(a) && Array.isArray(b)) {
89512
- if (a.length !== b.length) return false;
89513
- for (let i = 0; i < a.length; i++) if (!deepEqualIgnoreNoValue(a[i], b[i])) return false;
89514
- return true;
89515
- }
89516
- const aObj = a;
89517
- const bObj = b;
89518
- const aKeys = Object.keys(aObj).filter((k) => !isAwsNoValue(aObj[k]) && aObj[k] !== void 0);
89519
- const bKeys = Object.keys(bObj).filter((k) => !isAwsNoValue(bObj[k]) && bObj[k] !== void 0);
89520
- if (aKeys.length !== bKeys.length) return false;
89521
- const aKeySet = new Set(aKeys);
89522
- for (const k of bKeys) {
89523
- if (!aKeySet.has(k)) return false;
89524
- if (!deepEqualIgnoreNoValue(aObj[k], bObj[k])) return false;
89525
- }
89526
- return true;
89527
- }
89528
- /**
89529
- * True when the value is the CFn `AWS::NoValue` placeholder
89530
- * (`{Ref: 'AWS::NoValue'}`). `cdk migrate`'s codegen emits this for
89531
- * conditional properties the source template omitted; the structural
89532
- * intent matches and deep-equal must treat them as absent.
89533
- */
89534
- function isAwsNoValue(v) {
89535
- if (!v || typeof v !== "object") return false;
89536
- const obj = v;
89537
- const keys = Object.keys(obj);
89538
- return keys.length === 1 && keys[0] === "Ref" && obj["Ref"] === "AWS::NoValue";
89539
- }
89540
-
89541
- //#endregion
89542
- //#region src/cli/commands/migrate/resource-mapping-file.ts
89543
- /** Conventional filename inside the migrate output dir. */
89544
- const RESOURCE_MAPPING_FILENAME = "cdkd-resource-mapping.json";
89545
- /**
89546
- * Build the on-disk file shape from a {@link ResourceMappingResult} +
89547
- * the source / output stack names, then write it to
89548
- * `<outputDir>/cdkd-resource-mapping.json` (overwriting any prior file
89549
- * on the same path — same idempotency contract as `cdk migrate`'s own
89550
- * codegen, and the user expects a re-run to refresh stale data).
89551
- *
89552
- * Returns the absolute on-disk path so the orchestrator can name it in
89553
- * the error message when the mapping is incomplete.
89554
- */
89555
- function writeMappingFile(outputDir, args) {
89556
- const path = join(outputDir, RESOURCE_MAPPING_FILENAME);
89557
- const file = {
89558
- version: 1,
89559
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
89560
- sourceStack: args.sourceStack,
89561
- outputStack: args.outputStack,
89562
- mapping: args.result.mapping
89563
- };
89564
- if (args.result.unmatched.length > 0) file._unmatched = args.result.unmatched;
89565
- writeFileSync(path, JSON.stringify(file, null, 2) + "\n", "utf-8");
89566
- return path;
89567
- }
89568
- /**
89569
- * Read a user-supplied resource-mapping file from disk and return the
89570
- * `{<srcLogicalId>: <synthLogicalId>}` overrides for the mapper.
89571
- *
89572
- * Tolerates `_unmatched` on input (a user that re-runs against a
89573
- * partial-failure file without editing should hit the same failure on
89574
- * the resolution side — not a parse error here). Hard-errors on
89575
- * structurally invalid input (missing `mapping`, non-object root,
89576
- * non-string values, wrong `version`).
89577
- *
89578
- * Surfaces every shape error as `LocalMigrateError` (exit code 2) so
89579
- * the CLI handler routes it through the normal error path.
89580
- */
89581
- function readMappingFile(path) {
89582
- if (!existsSync(path)) throw new LocalMigrateError(`Resource-mapping file not found: ${path}. Drop the --resource-mapping flag or supply a valid path.`);
89583
- let raw;
89584
- try {
89585
- raw = JSON.parse(readFileSync(path, "utf-8"));
89586
- } catch (err) {
89587
- throw new LocalMigrateError(`Resource-mapping file '${path}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
89588
- }
89589
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new LocalMigrateError(`Resource-mapping file '${path}' must contain a JSON object at the top level.`);
89590
- const obj = raw;
89591
- if (obj["version"] !== 1) throw new LocalMigrateError(`Resource-mapping file '${path}' has unsupported version '${String(obj["version"])}'. Expected version 1.`);
89592
- if (typeof obj["sourceStack"] !== "string" || obj["sourceStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'sourceStack' field.`);
89593
- if (typeof obj["outputStack"] !== "string" || obj["outputStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'outputStack' field.`);
89594
- if (typeof obj["generatedAt"] !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'generatedAt' field.`);
89595
- const mappingRaw = obj["mapping"];
89596
- if (!mappingRaw || typeof mappingRaw !== "object" || Array.isArray(mappingRaw)) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'mapping' object.`);
89597
- const mapping = {};
89598
- for (const [k, v] of Object.entries(mappingRaw)) {
89599
- if (typeof v !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' has a non-string value for key '${k}'.`);
89600
- mapping[k] = v;
89601
- }
89602
- const result = {
89603
- version: 1,
89604
- generatedAt: obj["generatedAt"],
89605
- sourceStack: obj["sourceStack"],
89606
- outputStack: obj["outputStack"],
89607
- mapping
89608
- };
89609
- if (Array.isArray(obj["_unmatched"])) {
89610
- const cleaned = [];
89611
- for (const entry of obj["_unmatched"]) {
89612
- if (!entry || typeof entry !== "object") continue;
89613
- const e = entry;
89614
- if (typeof e["sourceLogicalId"] !== "string" || typeof e["resourceType"] !== "string" || !Array.isArray(e["candidates"]) || e["reason"] !== "no-match" && e["reason"] !== "logical-id-collision") continue;
89615
- cleaned.push({
89616
- sourceLogicalId: e["sourceLogicalId"],
89617
- resourceType: e["resourceType"],
89618
- candidates: e["candidates"].filter((c) => typeof c === "string"),
89619
- reason: e["reason"]
89620
- });
89621
- }
89622
- if (cleaned.length > 0) result._unmatched = cleaned;
89623
- }
89624
- return result;
89625
- }
89626
-
89627
- //#endregion
89628
- //#region src/cli/commands/migrate-command.ts
89629
- /**
89630
- * `cdkd migrate --from-cfn-stack <name>` end-to-end orchestrator.
89631
- *
89632
- * Closes the CLI half of [#465](https://github.com/go-to-k/cdkd/issues/465).
89633
- * Reads the source CFn stack, runs upstream `cdk migrate` via the PR A
89634
- * library, builds the source → synth logical-ID mapping with the 2-pass
89635
- * algorithm, writes a `cdkd-resource-mapping.json` audit file, prompts
89636
- * for confirmation, invokes `cdkd import` to write state under the
89637
- * synth logical IDs, and (when `--retire-cfn-stack` is set) finally
89638
- * retires the source CFn stack so management responsibility transfers
89639
- * fully to cdkd.
89640
- *
89641
- * AWS resources are never modified — the migration is a metadata
89642
- * transfer only. See [docs/design/465-cfn-migrate.md](../../../docs/design/465-cfn-migrate.md) §7
89643
- * for the post-migration state matrix.
89644
- */
89645
- async function migrateCommandAction(positionalStack, options) {
89646
- const logger = getLogger();
89647
- if (options.verbose) {
89648
- logger.setLevel("debug");
89649
- process.env["CDKD_NO_LIVE"] = "1";
89650
- }
89651
- const sourceCfnStackName = options.fromCfnStack ?? positionalStack;
89652
- if (!sourceCfnStackName) throw new LocalMigrateError("Missing required argument: --from-cfn-stack <name> (or pass the stack name positionally).");
89653
- if (options.retireCfnStack && options.skipSynth) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --skip-synth: the post-state-write retirement (UpdateStack + DeleteStack) cannot run without a synthesized template + cdkd state.");
89654
- if (options.retireCfnStack && options.dryRun) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --dry-run: retirement issues real AWS calls (UpdateStack injects Retain policies, then DeleteStack).");
89655
- if (options.retireCfnStack && options.filter && options.filter.length > 0) throw new LocalMigrateError("--retire-cfn-stack is incompatible with --filter: a partial migration that retires the whole source CFn stack would strand the un-migrated resources. Drop one of the flags or migrate the rest of the stack first.");
89656
- foldRegionOption(options);
89657
- await applyRoleArnIfSet({
89658
- roleArn: options.roleArn,
89659
- region: options.region
89660
- });
89661
- const region = namedCliRegion(options.region) ?? "us-east-1";
89662
- const outputDir = resolve(options.outputDir ?? sourceCfnStackName);
89663
- logger.info(`[migrate] Source CFn stack: ${sourceCfnStackName}`);
89664
- logger.info(`[migrate] Output directory: ${outputDir}`);
89665
- const libResult = await runMigrateLibrary({
89666
- fromCfnStack: sourceCfnStackName,
89667
- outputDir,
89668
- language: options.language ?? "typescript",
89669
- ...options.region && { region: options.region },
89670
- ...options.account && { account: options.account },
89671
- ...options.filter && options.filter.length > 0 && { filters: options.filter },
89672
- ...options.profile && { profile: options.profile },
89673
- ...options.cdkBin && { cdkBinPath: options.cdkBin },
89674
- skipInstall: options.skipInstall ?? false,
89675
- skipSynth: options.skipSynth ?? false
89676
- });
89677
- if (options.skipSynth || !libResult.templateBody) {
89678
- logger.info(`[migrate] --skip-synth: generated CDK app at ${libResult.outputDir}. Re-run without --skip-synth to write cdkd state.`);
89679
- return;
89680
- }
89681
- let overrides;
89682
- if (options.resourceMapping) {
89683
- const mappingPath = resolve(options.resourceMapping);
89684
- logger.info(`[migrate] Loading user-supplied resource mapping from ${mappingPath}`);
89685
- overrides = readMappingFile(mappingPath).mapping;
89686
- }
89687
- let mappingResult;
89688
- try {
89689
- mappingResult = buildResourceMapping({
89690
- sourceCfnTemplate: libResult.sourceCfnTemplate,
89691
- synthTemplate: libResult.templateBody,
89692
- sourceResources: libResult.sourceResources,
89693
- ...overrides && { overrides }
89694
- });
89695
- } catch (err) {
89696
- throw new LocalMigrateError(err instanceof Error ? err.message : String(err));
89697
- }
89698
- const outputStack = sourceCfnStackName;
89699
- const mappingFilePath = writeMappingFile(libResult.outputDir, {
89700
- sourceStack: sourceCfnStackName,
89701
- outputStack,
89702
- result: mappingResult
89703
- });
89704
- logger.info(`[migrate] Resource mapping written to ${mappingFilePath}`);
89705
- if (mappingResult.unmatched.length > 0) {
89706
- const lines = mappingResult.unmatched.map((u) => {
89707
- const candidatesStr = u.candidates.length > 0 ? ` (synth candidates of same Type: ${u.candidates.join(", ")})` : " (no synth resource has the same Type)";
89708
- return ` - ${u.sourceLogicalId} (${u.resourceType}) [${u.reason}]${candidatesStr}`;
89709
- });
89710
- throw new LocalMigrateError(`Could not auto-map ${mappingResult.unmatched.length} of ${mappingResult.unmatched.length + mappingResult.pairs.length} source resource(s) to the synthesized CDK template:\n${lines.join("\n")}\n\nEdit '${mappingFilePath}'\nto add the correct '<sourceLogicalId>: <synthLogicalId>' pairs under "mapping", then re-run:\n cdkd migrate --from-cfn-stack ${sourceCfnStackName} --resource-mapping ${mappingFilePath} --output-dir ${libResult.outputDir}\n(The --output-dir override re-uses the already-generated CDK code; drop it if you let the default location be used.)`);
89711
- }
89712
- printMappingTable(mappingResult, sourceCfnStackName, outputStack);
89713
- if (options.dryRun) {
89714
- logger.info(`[migrate] --dry-run: would import ${mappingResult.pairs.length} resource(s) into cdkd state under stack '${outputStack}' (region '${region}'). State NOT written.`);
89715
- return;
89716
- }
89717
- if (!options.yes) {
89718
- if (!await promptConfirm(`Import ${mappingResult.pairs.length} resource(s) into cdkd state for stack '${outputStack}' (${region})?`)) {
89719
- logger.info("[migrate] Cancelled by user. No state written.");
89720
- return;
89721
- }
89722
- }
89723
- const importMapping = {};
89724
- for (const p of mappingResult.pairs) importMapping[p.synthLogicalId] = p.physicalId;
89725
- const resolvedStateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
89726
- await runImport(outputStack, {
89727
- app: libResult.assemblyDir,
89728
- statePrefix: options.statePrefix ?? "cdkd",
89729
- resourceMappingInline: JSON.stringify(importMapping),
89730
- auto: false,
89731
- dryRun: false,
89732
- yes: true,
89733
- force: false,
89734
- verbose: options.verbose ?? false,
89735
- stateBucket: resolvedStateBucket,
89736
- ...options.region && { region: options.region },
89737
- ...options.profile && { profile: options.profile },
89738
- ...options.roleArn && { roleArn: options.roleArn }
89739
- });
89740
- if (options.retireCfnStack) {
89741
- logger.info(`[migrate] Retiring source CloudFormation stack '${sourceCfnStackName}'...`);
89742
- const cfnConfig = {};
89743
- if (options.region) cfnConfig.region = options.region;
89744
- if (options.profile) cfnConfig.profile = options.profile;
89745
- const awsClients = new AwsClients(cfnConfig);
89746
- const cfnClient = awsClients.cloudFormation;
89747
- try {
89748
- await retireCloudFormationStack({
89749
- cfnStackName: sourceCfnStackName,
89750
- cfnClient,
89751
- yes: options.yes ?? false,
89752
- stateBucket: resolvedStateBucket,
89753
- ...options.profile && { s3ClientOpts: { profile: options.profile } }
89754
- });
89755
- } finally {
89756
- awsClients.destroy();
89757
- }
89758
- }
89759
- logger.info(`[migrate] Migrated ${mappingResult.pairs.length} resource(s) from CloudFormation stack '${sourceCfnStackName}' to cdkd state for stack '${outputStack}'.`);
89760
- logger.info(`[migrate] Generated CDK app at ${libResult.outputDir}`);
89761
- if (!options.retireCfnStack) logger.info(`[migrate] Source CloudFormation stack '${sourceCfnStackName}' is unchanged. Re-run with --retire-cfn-stack to retire it (or do so manually later).`);
89762
- }
89763
- /**
89764
- * Print a tabular `(sourceLogicalId → synthLogicalId physicalId)`
89765
- * summary so users can see what's about to be imported before the
89766
- * confirmation prompt. Matches the visual shape of `cdkd import`'s
89767
- * summary table for consistency.
89768
- */
89769
- function printMappingTable(result, sourceStack, outputStack) {
89770
- const logger = getLogger();
89771
- logger.info("");
89772
- logger.info(`[migrate] Resolved mapping (${sourceStack} → ${outputStack}):`);
89773
- for (const p of result.pairs) logger.info(` ${p.sourceLogicalId} → ${p.synthLogicalId} [${p.resourceType}] ${p.physicalId}`);
89774
- logger.info("");
89775
- }
89776
- /**
89777
- * Minimal `(y/N)` confirmation prompt using `node:readline`. Mirrors
89778
- * the shape of [src/cli/commands/import.ts](import.ts)'s `confirmPrompt`
89779
- * so the user UX is consistent across the import + migrate surfaces.
89780
- * Non-TTY callers (CI) should pass `--yes` to skip this entirely.
89781
- */
89782
- async function promptConfirm(message) {
89783
- if (!process.stdin.isTTY) throw new LocalMigrateError(`Non-interactive shell detected and --yes was not supplied. Re-run with --yes to confirm: "${message}"`);
89784
- const rl = (await import("node:readline/promises")).createInterface({
89785
- input: process.stdin,
89786
- output: process.stdout
89787
- });
89788
- try {
89789
- const v = (await rl.question(`${message} (y/N) `)).trim().toLowerCase();
89790
- return v === "y" || v === "yes";
89791
- } finally {
89792
- rl.close();
89793
- }
89794
- }
89795
- /**
89796
- * Commander factory for `cdkd migrate`. Registered in `src/cli/index.ts`
89797
- * alongside the existing top-level commands. Matches the design doc §3
89798
- * flag set; the action handler routes through {@link migrateCommandAction}
89799
- * via `withErrorHandling` so library callers see exceptions but CLI
89800
- * callers get the standard exit-code-2 routing.
89801
- */
89802
- function createMigrateCommand() {
89803
- return new Command("migrate").description("Adopt a plain (non-CDK) CloudFormation stack into a cdkd-managed CDK app. Generates new CDK code via upstream `cdk migrate`, builds a logical-ID mapping between the source CFn template and the synth template, writes cdkd state, and (optionally) retires the source CFn stack. AWS resources are never modified.").argument("[stack]", "Source CFn stack name. Alias for --from-cfn-stack.").addOption(new Option("--from-cfn-stack <name>", "Source CloudFormation stack name to adopt. Required (or pass positionally).")).addOption(new Option("--output-dir <dir>", "Directory to write the generated CDK app to. Defaults to <cwd>/<CfnStackName>.")).addOption(new Option("--language <choice>", "Generated code language. v1: typescript only.").choices(["typescript"]).default("typescript")).addOption(new Option("--region <region>", "AWS region. Defaults to AWS_REGION env / profile.")).addOption(new Option("--account <id>", "AWS account ID. Auto-detected via STS when omitted.")).addOption(new Option("--retire-cfn-stack", "After cdkd state is written, inject DeletionPolicy=Retain on every resource in the source CFn stack and DeleteStack. AWS resources stay; the CFn stack record is gone. Off by default.").default(false)).addOption(new Option("--filter <key=value>", "Pass-through to `cdk migrate --filter` for resource subsetting. Repeatable.").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--skip-install", "Skip `npm install` after codegen.").default(false)).addOption(new Option("--skip-synth", "Skip `cdk synth` (does NOT write cdkd state). Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("--dry-run", "Print the import plan without writing state or retiring the CFn stack. Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("-y, --yes", "Auto-confirm the import + retirement prompts.").default(false)).addOption(new Option("--cdk-bin <path>", "Override the `cdk` binary path.")).addOption(new Option("--resource-mapping <file>", `Path to a JSON file of {sourceLogicalId: synthLogicalId} overrides. Same shape as the auto-written ${RESOURCE_MAPPING_FILENAME}.`)).addOption(new Option("--state-bucket <name>", "cdkd state bucket. Defaults to cdkd-state-<accountId>.")).addOption(new Option("--state-prefix <prefix>", "cdkd state prefix inside the bucket.").default(DEFAULT_STATE_PREFIX)).addOption(new Option("--profile <name>", "AWS profile name.")).addOption(new Option("--role-arn <arn>", "IAM role to assume before any AWS call.")).addOption(new Option("--verbose", "Enable debug-level logging.").default(false)).action(withErrorHandling(migrateCommandAction));
89804
- }
89805
-
89806
88908
  //#endregion
89807
88909
  //#region src/cli/program.ts
89808
88910
  /**
@@ -89846,11 +88948,10 @@ function buildProgram() {
89846
88948
  program.addCommand(createStateCommand());
89847
88949
  program.addCommand(createLocalCommand());
89848
88950
  program.addCommand(createExportCommand());
89849
- program.addCommand(createMigrateCommand());
89850
88951
  guardStackRegionOptions(program);
89851
88952
  return program;
89852
88953
  }
89853
88954
 
89854
88955
  //#endregion
89855
88956
  export { buildProgram };
89856
- //# sourceMappingURL=program-C-xfsrSP.js.map
88957
+ //# sourceMappingURL=program-x67eZ06V.js.map