@go-to-k/cdkd 0.285.16 → 0.286.0

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-DYYs56Gt.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-Dkd6KwPz.js";
5
+ import { t as ASGProvider } from "./asg-provider-D9Lc_ngz.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,162 @@ 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 = [];
2713
2759
  for (const target of targets) {
2714
- if (target.resourceType !== "AWS::S3::Bucket" || target.statefulReason !== null) {
2760
+ if (target.statefulReason !== null) {
2715
2761
  promoted.push({ ...target });
2716
2762
  continue;
2717
2763
  }
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 });
2764
+ if (target.resourceType === "AWS::S3::Bucket") {
2765
+ try {
2766
+ const result = await clients.s3.send(new ListObjectVersionsCommand({
2767
+ Bucket: target.physicalId,
2768
+ MaxKeys: 1
2769
+ }));
2770
+ const hasVersions = (result.Versions?.length ?? 0) > 0;
2771
+ const hasDeleteMarkers = (result.DeleteMarkers?.length ?? 0) > 0;
2772
+ if (hasVersions || hasDeleteMarkers) promoted.push({
2773
+ ...target,
2774
+ statefulReason: "has-objects"
2775
+ });
2776
+ else promoted.push({ ...target });
2777
+ } catch (e) {
2778
+ 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)}`);
2779
+ promoted.push({ ...target });
2780
+ }
2781
+ continue;
2733
2782
  }
2783
+ if (target.resourceType === "AWS::Logs::LogGroup") {
2784
+ try {
2785
+ const result = await clients.cloudWatchLogs.send(new DescribeLogStreamsCommand({
2786
+ logGroupName: target.physicalId,
2787
+ limit: 1
2788
+ }));
2789
+ const streams = result.logStreams;
2790
+ if (streams !== void 0 && streams.length === 0 && !result.nextToken) promoted.push({ ...target });
2791
+ else if (streams !== void 0 && streams.length > 0) promoted.push({
2792
+ ...target,
2793
+ statefulReason: "has-log-events"
2794
+ });
2795
+ else {
2796
+ 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).`);
2797
+ promoted.push({
2798
+ ...target,
2799
+ statefulReason: "has-log-events"
2800
+ });
2801
+ }
2802
+ } catch (e) {
2803
+ if (e instanceof ResourceNotFoundException$5) {
2804
+ let regionVerified = true;
2805
+ const recordedRegion = foldRegion(clients.expectedRegion);
2806
+ if (recordedRegion) {
2807
+ let clientRegion;
2808
+ try {
2809
+ clientRegion = foldRegion(await clients.cloudWatchLogs.config.region());
2810
+ assertRegionMatch(clientRegion, recordedRegion, target.resourceType, target.logicalId, target.physicalId);
2811
+ } catch {
2812
+ regionVerified = false;
2813
+ 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`;
2814
+ 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.`);
2815
+ }
2816
+ }
2817
+ if (regionVerified) {
2818
+ 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.`);
2819
+ promoted.push({ ...target });
2820
+ continue;
2821
+ }
2822
+ promoted.push({
2823
+ ...target,
2824
+ statefulReason: "has-log-events"
2825
+ });
2826
+ continue;
2827
+ }
2828
+ 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)}`);
2829
+ promoted.push({
2830
+ ...target,
2831
+ statefulReason: "has-log-events"
2832
+ });
2833
+ }
2834
+ continue;
2835
+ }
2836
+ promoted.push({ ...target });
2734
2837
  }
2735
2838
  return promoted;
2736
2839
  }
2737
2840
  /**
2738
2841
  * Async re-validation of the stateful-guard slice of a
2739
- * {@link RecreateTargetsValidation}, after promoting S3 bucket reasons
2740
- * via {@link probeStatefulRecreateTargetsAsync}.
2842
+ * {@link RecreateTargetsValidation}, after promoting the deferred S3 bucket
2843
+ * and log group reasons via {@link probeStatefulRecreateTargetsAsync}.
2741
2844
  *
2742
2845
  * Skips the probe entirely when `forceStatefulRecreation: true` — the
2743
2846
  * sync validation already omits the blocked list in that case, and
@@ -2750,7 +2853,7 @@ async function probeStatefulRecreateTargetsAsync(targets, s3Client, logger = get
2750
2853
  */
2751
2854
  async function probeAndRevalidateStateful(input) {
2752
2855
  if (input.forceStatefulRecreation) return input.validation;
2753
- const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.s3Client);
2856
+ const promoted = await probeStatefulRecreateTargetsAsync(input.validation.targets, input.clients);
2754
2857
  const blockedStatefulTargets = promoted.filter((t) => t.statefulReason !== null);
2755
2858
  return {
2756
2859
  ...input.validation,
@@ -2853,11 +2956,27 @@ function renderDownstreamConsumers(producerStack, consumers) {
2853
2956
  * like a successful skipped-deploy.
2854
2957
  *
2855
2958
  * 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.
2959
+ * targets; a target reaches this prompt still carrying a stateful reason only
2960
+ * because the user opted in with `--force-stateful-recreation`, so the
2961
+ * **DATA LOSS** wording is the third "stop and think" moment.
2962
+ *
2963
+ * **The prefix is NOT read off `statefulReason` alone**, and the reason is the
2964
+ * flag that makes the prompt reachable in the first place (issue [#2558]'s
2965
+ * review round). `probeAndRevalidateStateful` returns EARLY under
2966
+ * `--force-stateful-recreation` — it never issues the live
2967
+ * `s3:ListObjectVersions` / `logs:DescribeLogStreams` probes — so under that
2968
+ * flag every target still carries the SYNC verdict, and for the two
2969
+ * CONDITIONAL types the sync verdict of a deferral is `null`. `null` there
2970
+ * does not mean "holds nothing": an `AWS::S3::Bucket` always defers, and an
2971
+ * `AWS::Logs::LogGroup` with no recorded retention is CloudWatch Logs'
2972
+ * NEVER EXPIRE. Rendering that as "not stateful" is precisely the reading
2973
+ * issue [#2558] exists to retire, and it was already wrong for a non-empty
2974
+ * bucket before that. So when the probe did not run, a `null` reason is
2975
+ * re-derived through {@link isStatefulRecreateTargetForReplace} — the
2976
+ * conservative mid-deploy predicate, a pure function of the type with no AWS
2977
+ * call, chosen over re-running the probe because re-running it under the force
2978
+ * flag would repopulate `blockedStatefulTargets` and turn the opt-in into a
2979
+ * refusal.
2861
2980
  */
2862
2981
  async function promptRecreateConfirm(input) {
2863
2982
  if (input.targets.length === 0) return true;
@@ -2869,10 +2988,13 @@ async function promptRecreateConfirm(input) {
2869
2988
  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
2989
  else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
2871
2990
  for (const t of input.targets) {
2872
- const stateful = t.statefulReason !== null;
2991
+ const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0) : null;
2992
+ const rederived = rederivedReason !== null;
2993
+ const reason = t.statefulReason ?? rederivedReason;
2994
+ const stateful = reason !== null;
2873
2995
  const dataLossPrefix = stateful ? "**DATA LOSS** " : "";
2874
2996
  const directionTag = t.direction === "to-cc-api" ? " [SDK → CC]" : " [CC → SDK]";
2875
- const stateNote = stateful ? ` — stateful (${t.statefulReason}); --force-stateful-recreation acknowledged` : "";
2997
+ const stateNote = stateful ? ` — stateful (${rederived ? "emptiness not established — --force-stateful-recreation skips the probe" : renderStatefulReason(reason)}); --force-stateful-recreation acknowledged` : "";
2876
2998
  logger.warn(` - ${dataLossPrefix}${t.logicalId} (${t.resourceType})${directionTag}${stateNote}`);
2877
2999
  if (stateful) logger.warn(` DATA: all data in ${t.logicalId} will be lost (no automatic data migration)`);
2878
3000
  }
@@ -2950,12 +3072,13 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2950
3072
  * `printf 'y' |` (a real answer with no trailing newline) and `< /dev/null`
2951
3073
  * stay pending indefinitely.
2952
3074
  *
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
3075
+ * REFUSE rather than auto-confirm, at every one of the TEN sites that route
3076
+ * through this helper. Every one of them guards a MUTATION — a rollback
3077
+ * replay, a state record removal, an observed-property refresh, an orphan, an
3078
+ * import, an export-then-delete-state, a drift accept/revert, a
3079
+ * CloudFormation stack retirement, a state-bucket migration, an event-history
3080
+ * prune — so silently answering "yes" on behalf of an absent operator is never
3081
+ * the safe reading. `deploy.ts`'s
2959
3082
  * auto-confirm (`options.yes || !process.stdin.isTTY` -> proceed) stays the
2960
3083
  * deliberate exception, because a deploy that assumes "yes" is recoverable.
2961
3084
  *
@@ -2974,10 +3097,12 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2974
3097
  * `--yes` refuse instead of proceed, which is the inverse bug.
2975
3098
  *
2976
3099
  * 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.
3100
+ * `gc.ts` and `bootstrap-destroy.ts` — the only two of the four originally
3101
+ * guarded prompts that carry a code (the other two throw a bare `Error`).
3102
+ * `handleError` maps it to exit 1, so CI can branch on it.
3103
+ *
3104
+ * There were FIVE until `cdkd migrate` was removed (issue #2572); its prompt
3105
+ * threw a `LocalMigrateError`, and that class went with it.
2981
3106
  *
2982
3107
  * TWO GUARDED PROMPTS DELIBERATELY DO NOT ROUTE THROUGH HERE:
2983
3108
  * `destroy-runner.ts`'s per-stack prompt (the issue #2259 fix) and
@@ -2988,10 +3113,10 @@ const DEFAULT_CONFIRM_SUFFIX = " [y/N] ";
2988
3113
  * passes an abort `signal` for the issue #2117 Ctrl-C handling. Folding them
2989
3114
  * in would also drag `destroy-runner.ts` (in the `integ-destroy` AND
2990
3115
  * `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
3116
+ * real-AWS run for no behaviour change. The other four pre-existing guarded
2992
3117
  * 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.
3118
+ * `prefix-migration-check.ts`) stay put for the same reason: each guards its
3119
+ * own flow with its own error type.
2995
3120
  *
2996
3121
  * @param prompt the question, WITHOUT its trailing `[y/N]` marker
2997
3122
  * @returns `true` only for `y` / `yes` (any case, trimmed)
@@ -21731,9 +21856,8 @@ function warmThroughputDiagnostic(raw, coercion, indexName) {
21731
21856
  * The CFn schema forbids `ProvisionedThroughput` / `OnDemandThroughput` /
21732
21857
  * `*ThroughputOverride` on `AWS::DynamoDB::GlobalTable`, so this only fires
21733
21858
  * 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
21859
+ * first-class cdkd surface (`cdkd import --migrate-from-cloudformation`), and
21860
+ * the pre-fix code forwarded such a block VERBATIM. Three defects followed, and fixing any subset leaves a
21737
21861
  * live one, which is why this helper does all three at once:
21738
21862
  *
21739
21863
  * 1. **Coercion.** The verbatim forward was the ONE path that skipped
@@ -22937,7 +23061,7 @@ var LogsLogGroupProvider = class {
22937
23061
  const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
22938
23062
  const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
22939
23063
  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`);
23064
+ 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
23065
  const newKmsKeyId = properties["KmsKeyId"];
22942
23066
  if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
22943
23067
  if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
@@ -47289,9 +47413,32 @@ function isAccountIdPseudoParameter(value) {
47289
47413
  * [#1675](https://github.com/go-to-k/cdkd/issues/1675)).
47290
47414
  *
47291
47415
  * 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
47416
+ * load-bearing rather than defensive, and the reason is NOT the one this
47417
+ * comment gave for three revisions. It claimed
47418
+ * `cdkd import --migrate-from-cloudformation` (earlier: `cdkd migrate`) read
47419
+ * the source stack's ORIGINAL CloudFormation template and handed those raw
47420
+ * properties to providers. It never did. Both commands take properties from
47421
+ * the SYNTH assembly; `cdkd migrate` reached `runImport` with
47422
+ * `app: <assemblyDir>` plus a logical-id mapping, and the ORIGINAL template it
47423
+ * fetched was consumed only by transform detection and by the Pass-2
47424
+ * property-equality MATCH, never forwarded. The `GetTemplate` that survives in
47425
+ * `retire-cfn-stack.ts` feeds Retain injection and the re-upload, and reaches
47426
+ * no properties bag either. Do not restore any of that.
47427
+ *
47428
+ * The two producers that ARE real, both current:
47429
+ *
47430
+ * 1. A numeric literal in the synth template itself — `new CfnResource({
47431
+ * properties: { CatalogId: 123456789012 } })`, or an
47432
+ * `addPropertyOverride`. CDK emits the number verbatim; nothing between
47433
+ * synth and this provider stringifies it.
47434
+ * 2. The macro-expanded deploy template. `src/synthesis/macro-expander.ts`
47435
+ * fetches `GetTemplate` at `TemplateStage: 'Processed'` and parses the
47436
+ * JSON body as the template deploy / diff / import then use, so a macro
47437
+ * that emits an unquoted number lands here as a number.
47438
+ *
47439
+ * The DELETE path reaches it too, from the other side: `destroy-runner.ts`
47440
+ * hands the recorded `resource.properties` straight to `provider.delete`, so a
47441
+ * state row holding either shape above replays it at teardown. Rejecting it
47295
47442
  * as "not a string" would drop the field and silently retarget the call at the
47296
47443
  * account's DEFAULT catalog — which on a DELETE can destroy a same-named
47297
47444
  * resource that happens to live there. `Number.isFinite` keeps `NaN` /
@@ -49041,9 +49188,12 @@ const TARGET_DATABASE_MEMBERS = [
49041
49188
  const FEDERATED_DATABASE_MEMBERS = ["ConnectionName", "Identifier"];
49042
49189
  /**
49043
49190
  * 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.
49191
+ * is stringly typed, and an account id can reach a provider as a JSON number
49192
+ * (the reason {@link catalogIdForApi} exists; that function's note carries the
49193
+ * two producers, neither of which is a YAML read — an earlier version of this
49194
+ * sentence said "an unquoted YAML account id" and was wrong the same way) —
49195
+ * and nothing else, so an unresolved intrinsic cannot ride an unchecked cast
49196
+ * into the request.
49047
49197
  */
49048
49198
  const isSendableLeaf = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
49049
49199
  /**
@@ -62468,7 +62618,11 @@ async function deployCommand(stacks, options) {
62468
62618
  forceStatefulRecreation: options.forceStatefulRecreation ?? false,
62469
62619
  hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
62470
62620
  }),
62471
- s3Client: stackAwsClients.s3,
62621
+ clients: {
62622
+ s3: stackAwsClients.s3,
62623
+ cloudWatchLogs: stackAwsClients.cloudWatchLogs,
62624
+ expectedRegion: stateForRecreateCheck?.state.region
62625
+ },
62472
62626
  forceStatefulRecreation: options.forceStatefulRecreation ?? false
62473
62627
  });
62474
62628
  const errorBlock = renderRecreateTargetsErrors(validation);
@@ -62486,6 +62640,7 @@ async function deployCommand(stacks, options) {
62486
62640
  stackName: stackInfo.stackName,
62487
62641
  targets: validation.targets,
62488
62642
  yes: options.yes ?? false,
62643
+ forceStatefulRecreation: options.forceStatefulRecreation ?? false,
62489
62644
  downstreamConsumers
62490
62645
  })) return;
62491
62646
  }
@@ -63040,10 +63195,11 @@ function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /*
63040
63195
  * unhealthy state — callers gate AWS-side mutations behind this set so
63041
63196
  * the user can settle the source before paying for further work.
63042
63197
  *
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`).
63198
+ * Consumed by the `cdkd import --migrate-from-cloudformation` retirement
63199
+ * flow (`src/cli/commands/retire-cfn-stack.ts`). It had a second consumer,
63200
+ * `cdkd migrate`'s pre-flight check, until that command was removed (issue
63201
+ * #2572); the module stays separate so the next CloudFormation-reading
63202
+ * surface shares the set instead of re-spelling it.
63047
63203
  */
63048
63204
  const STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
63049
63205
  "CREATE_COMPLETE",
@@ -63857,20 +64013,20 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
63857
64013
  }
63858
64014
  /**
63859
64015
  * 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
64016
+ * `cdkd import --migrate-from-cloudformation` (skip flag `-y` / `--yes`). Its
64017
+ * only call site is inside the `if (!yes)` block above, which is what keeps
63863
64018
  * `confirmOrRefuse`'s non-interactive refusal (issue #2275) from firing on a
63864
64019
  * `--yes` run.
63865
64020
  *
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.
64021
+ * The refusal NAMES that command, as every other site does, because
64022
+ * `confirmOrRefuse`'s contract is that the refusal is the only thing a CI user
64023
+ * sees: it has to say which invocation the missing flag belongs on. It named
64024
+ * TWO commands until `cdkd migrate --retire-cfn-stack` was removed with the
64025
+ * command itself (issue #2572); `cdkd import --migrate-from-cloudformation` is
64026
+ * now the sole way to reach this prompt.
63871
64027
  *
63872
64028
  * 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
64029
+ * only one of the ten that fires AFTER its command's state write, so a
63874
64030
  * refusal here leaves a genuine split brain — cdkd claims the resources while
63875
64031
  * the CloudFormation stack is still live — and the operator has to know that
63876
64032
  * before deciding whether to re-run or finish by hand.
@@ -63878,7 +64034,7 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
63878
64034
  * Exported for unit testing — internal to the command flow otherwise.
63879
64035
  */
63880
64036
  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." });
64037
+ 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
64038
  }
63883
64039
  /**
63884
64040
  * Flat `logicalId -> physicalId` map for ONE CloudFormation stack, or `null`
@@ -78732,6 +78888,7 @@ async function forceUnlockCommand(stackArgs, options) {
78732
78888
  setAwsClients(awsClients);
78733
78889
  const region = namedCliRegion(options.region) ?? "us-east-1";
78734
78890
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
78891
+ const failures = [];
78735
78892
  try {
78736
78893
  const stateConfig = {
78737
78894
  bucket: stateBucket,
@@ -78756,13 +78913,17 @@ async function forceUnlockCommand(stackArgs, options) {
78756
78913
  } catch (error) {
78757
78914
  const message = error instanceof Error ? error.message : String(error);
78758
78915
  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}`);
78916
+ else {
78917
+ logger.error(`Failed to unlock stack ${where}: ${message}`);
78918
+ failures.push(`${where}: ${message}`);
78919
+ }
78760
78920
  }
78761
78921
  }
78762
78922
  }
78763
78923
  } finally {
78764
78924
  awsClients.destroy();
78765
78925
  }
78926
+ 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
78927
  }
78767
78928
  /**
78768
78929
  * Create force-unlock command
@@ -78780,9 +78941,6 @@ function createForceUnlockCommand() {
78780
78941
 
78781
78942
  //#endregion
78782
78943
  //#region src/cli/commands/import.ts
78783
- async function runImport(stackArg, options) {
78784
- return importCommand(stackArg, options);
78785
- }
78786
78944
  async function importCommand(stackArg, options) {
78787
78945
  const logger = getLogger();
78788
78946
  if (options.verbose) {
@@ -79667,7 +79825,7 @@ function validateNestedStackShape(template, tree, parentStackName, nestedTemplat
79667
79825
  * **parent-first acquire, leaves-first release** (each level's
79668
79826
  * `finally` releases its child lock before sibling iteration
79669
79827
  * continues, and the root lock is the outermost — held by
79670
- * `runImport`'s `try` / `finally`). This is the conventional
79828
+ * `importCommand`'s `try` / `finally`). This is the conventional
79671
79829
  * hierarchical lock pattern: parent-first acquire prevents a
79672
79830
  * second cdkd import from racing past the root lock; leaves-first
79673
79831
  * release on success/failure means a mid-walk error never strands
@@ -81890,7 +82048,7 @@ async function applyCrossStackResolverToTask(task, context) {
81890
82048
 
81891
82049
  //#endregion
81892
82050
  //#region src/local/docker-runner.ts
81893
- const execFileAsync$4 = promisify(execFile);
82051
+ const execFileAsync$3 = promisify(execFile);
81894
82052
  /**
81895
82053
  * Wraps `docker pull` / `docker run` / `docker rm` for `cdkd local invoke`.
81896
82054
  *
@@ -81994,7 +82152,7 @@ async function runDetached(opts) {
81994
82152
  logger.debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
81995
82153
  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
82154
  try {
81997
- const { stdout } = await execFileAsync$4(getDockerCmd(), args, {
82155
+ const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
81998
82156
  maxBuffer: 10 * 1024 * 1024,
81999
82157
  env: dockerSpawnEnvWithSensitive(passthroughEnv)
82000
82158
  });
@@ -82034,7 +82192,7 @@ async function removeContainer(containerId) {
82034
82192
  if (!containerId) return;
82035
82193
  const logger = getLogger().child("docker");
82036
82194
  try {
82037
- await execFileAsync$4(getDockerCmd(), [
82195
+ await execFileAsync$3(getDockerCmd(), [
82038
82196
  "rm",
82039
82197
  "-f",
82040
82198
  containerId
@@ -82053,7 +82211,7 @@ async function removeContainer(containerId) {
82053
82211
  async function ensureDockerAvailable() {
82054
82212
  const cmd = getDockerCmd();
82055
82213
  try {
82056
- await execFileAsync$4(cmd, [
82214
+ await execFileAsync$3(cmd, [
82057
82215
  "version",
82058
82216
  "--format",
82059
82217
  "{{.Server.Version}}"
@@ -85067,7 +85225,7 @@ function createLocalStartApiCommand() {
85067
85225
 
85068
85226
  //#endregion
85069
85227
  //#region src/local/ecs-network.ts
85070
- const execFileAsync$3 = promisify(execFile);
85228
+ const execFileAsync$2 = promisify(execFile);
85071
85229
  /**
85072
85230
  * Docker network + AWS-published metadata-endpoints sidecar lifecycle for
85073
85231
  * `cdkd local run-task`. The sidecar (a small Go binary maintained by
@@ -85120,7 +85278,7 @@ async function createNetworkAndSidecar(args) {
85120
85278
  await pullImage(METADATA_ENDPOINT_IMAGE, skipPull);
85121
85279
  logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
85122
85280
  try {
85123
- await execFileAsync$3(getDockerCmd(), [
85281
+ await execFileAsync$2(getDockerCmd(), [
85124
85282
  "network",
85125
85283
  "create",
85126
85284
  "--driver",
@@ -85157,7 +85315,7 @@ async function createNetworkAndSidecar(args) {
85157
85315
  sidecarArgs.push(METADATA_ENDPOINT_IMAGE);
85158
85316
  logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
85159
85317
  try {
85160
- const { stdout } = await execFileAsync$3(getDockerCmd(), sidecarArgs, {
85318
+ const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, {
85161
85319
  maxBuffer: 10 * 1024 * 1024,
85162
85320
  env: dockerSpawnEnvWithSensitive(sidecarSensitiveEnv)
85163
85321
  });
@@ -85228,7 +85386,7 @@ async function destroyNetworkOnly(networkName) {
85228
85386
  if (!networkName) return;
85229
85387
  const logger = getLogger().child("ecs-network");
85230
85388
  try {
85231
- await execFileAsync$3(getDockerCmd(), [
85389
+ await execFileAsync$2(getDockerCmd(), [
85232
85390
  "network",
85233
85391
  "rm",
85234
85392
  networkName
@@ -85368,7 +85526,7 @@ async function resolveSsm(entry, shape, client) {
85368
85526
 
85369
85527
  //#endregion
85370
85528
  //#region src/local/ecs-task-runner.ts
85371
- const execFileAsync$2 = promisify(execFile);
85529
+ const execFileAsync$1 = promisify(execFile);
85372
85530
  /**
85373
85531
  * Top-level orchestrator for `cdkd local run-task`. Coordinates image
85374
85532
  * preparation, secret resolution, docker-network bring-up, container
@@ -85428,7 +85586,7 @@ async function cleanupEcsRun(state, options) {
85428
85586
  if (state.network && !state.network.ownedByCaller) await destroyTaskNetwork(state.network);
85429
85587
  state.network = void 0;
85430
85588
  for (const v of state.dockerVolumeNames) try {
85431
- await execFileAsync$2(getDockerCmd(), [
85589
+ await execFileAsync$1(getDockerCmd(), [
85432
85590
  "volume",
85433
85591
  "rm",
85434
85592
  v
@@ -85523,7 +85681,7 @@ async function runEcsTask(task, options, state) {
85523
85681
  let id;
85524
85682
  try {
85525
85683
  const sensitiveEnv = dockerEnvs.get(container.name);
85526
- const { stdout } = await execFileAsync$2(getDockerCmd(), args, {
85684
+ const { stdout } = await execFileAsync$1(getDockerCmd(), args, {
85527
85685
  maxBuffer: 10 * 1024 * 1024,
85528
85686
  env: dockerSpawnEnvWithSensitive(sensitiveEnv)
85529
85687
  });
@@ -85635,7 +85793,7 @@ async function waitForContainerHealthy(containerId, displayName) {
85635
85793
  let lastStatus = "";
85636
85794
  while (Date.now() < deadline) {
85637
85795
  try {
85638
- const { stdout } = await execFileAsync$2(getDockerCmd(), [
85796
+ const { stdout } = await execFileAsync$1(getDockerCmd(), [
85639
85797
  "inspect",
85640
85798
  "--format",
85641
85799
  "{{.State.Health.Status}}",
@@ -85658,7 +85816,7 @@ async function waitForContainerHealthy(containerId, displayName) {
85658
85816
  }
85659
85817
  async function waitForContainerExit(containerId) {
85660
85818
  try {
85661
- const { stdout } = await execFileAsync$2(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
85819
+ const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
85662
85820
  const code = Number.parseInt(stdout.trim(), 10);
85663
85821
  return Number.isFinite(code) ? code : 1;
85664
85822
  } catch (err) {
@@ -85668,7 +85826,7 @@ async function waitForContainerExit(containerId) {
85668
85826
  }
85669
85827
  async function stopContainer(containerId, graceSeconds) {
85670
85828
  try {
85671
- await execFileAsync$2(getDockerCmd(), [
85829
+ await execFileAsync$1(getDockerCmd(), [
85672
85830
  "stop",
85673
85831
  "-t",
85674
85832
  String(graceSeconds),
@@ -85793,7 +85951,7 @@ async function realizeDockerVolumes(volumes, state) {
85793
85951
  const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
85794
85952
  args.push(dockerVolumeName);
85795
85953
  try {
85796
- await execFileAsync$2(getDockerCmd(), args);
85954
+ await execFileAsync$1(getDockerCmd(), args);
85797
85955
  state.dockerVolumeNames.push(dockerVolumeName);
85798
85956
  logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
85799
85957
  } catch (err) {
@@ -86374,7 +86532,7 @@ function createLocalStartServiceCommand() {
86374
86532
  * after each reload (cdkd-specific — cdk-local treats single-shot as a
86375
86533
  * no-op WARN).
86376
86534
  */
86377
- const execFileAsync$1 = promisify(execFile);
86535
+ const execFileAsync = promisify(execFile);
86378
86536
  /**
86379
86537
  * Run the `--watch` reload loop. Returns when the invocation closes naturally
86380
86538
  * with no pending reload (one-shot finished, or `--ws` agent closed), or when
@@ -86525,7 +86683,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86525
86683
  const dockerCmd = getDockerCmd();
86526
86684
  let workdir;
86527
86685
  try {
86528
- const { stdout } = await execFileAsync$1(dockerCmd, [
86686
+ const { stdout } = await execFileAsync(dockerCmd, [
86529
86687
  "inspect",
86530
86688
  "--format",
86531
86689
  "{{.Config.WorkingDir}}",
@@ -86538,7 +86696,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86538
86696
  const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
86539
86697
  logger.info(`Soft-reload: docker cp ${newAssetSourceDir} -> ${containerId}:${workdirDest}; restart.`);
86540
86698
  try {
86541
- await execFileAsync$1(dockerCmd, [
86699
+ await execFileAsync(dockerCmd, [
86542
86700
  "cp",
86543
86701
  `${newAssetSourceDir}/.`,
86544
86702
  `${containerId}:${workdirDest}`
@@ -86547,7 +86705,7 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
86547
86705
  throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
86548
86706
  }
86549
86707
  try {
86550
- await execFileAsync$1(dockerCmd, ["restart", containerId]);
86708
+ await execFileAsync(dockerCmd, ["restart", containerId]);
86551
86709
  } catch (err) {
86552
86710
  throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
86553
86711
  }
@@ -88733,1076 +88891,6 @@ function createLocalCommand() {
88733
88891
  return local;
88734
88892
  }
88735
88893
 
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
88894
  //#endregion
89807
88895
  //#region src/cli/program.ts
89808
88896
  /**
@@ -89846,11 +88934,10 @@ function buildProgram() {
89846
88934
  program.addCommand(createStateCommand());
89847
88935
  program.addCommand(createLocalCommand());
89848
88936
  program.addCommand(createExportCommand());
89849
- program.addCommand(createMigrateCommand());
89850
88937
  guardStackRegionOptions(program);
89851
88938
  return program;
89852
88939
  }
89853
88940
 
89854
88941
  //#endregion
89855
88942
  export { buildProgram };
89856
- //# sourceMappingURL=program-C-xfsrSP.js.map
88943
+ //# sourceMappingURL=program-BXXm1RTv.js.map