@go-to-k/cdkd 0.133.0 → 0.135.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.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { _ as withSkipPrefix, a as runDockerStreaming, c as getLogger, d as getLiveRenderer, f as PATTERN_B_NAME_PROPERTIES, g as generateResourceNameWithFallback, h as generateResourceName, i as runDockerForeground, n as formatDockerLoginError, p as PATTERN_B_RESOURCE_TYPES, r as getDockerCmd, u as runStackBuffered, v as withStackName } from "./docker-cmd-EtWSTAje.js";
3
- import { $ as PartialFailureError, A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as resolveBucketRegion, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as AssemblyReader, V as resolveStateBucketWithDefaultAndSource, X as LocalInvokeBuildError, Z as LocalStartServiceError, _ as normalizeAwsTagsToCfn, a as withRetry, at as StackTerminationProtectionError, b as CloudControlProvider, c as cyan, d as red, dt as withErrorHandling, et as ProvisioningError, f as yellow, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, it as StackHasActiveImportsError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ResourceUpdateNotSupportedError, o as IMPLICIT_DELETE_DEPENDENCIES, p as IAMRoleProvider, q as CdkdError, r as DeployEngine, rt as RouteDiscoveryError, s as bold, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as ResourceTimeoutError, u as green, ut as normalizeAwsError, v as resolveExplicitPhysicalId, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-Dn7oV5rA.js";
3
+ import { A as AssetPublisher, B as resolveStateBucketWithDefault, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as getDefaultStateBucketName, G as resolveBucketRegion, H as warnDeprecatedNoPrefixCliFlag, I as getLegacyStateBucketName, L as resolveApp, M as WorkGraph, N as buildDockerImage, O as S3StateBackend, P as Synthesizer, Q as LocalStartServiceError, R as resolveCaptureObservedState, S as IntrinsicFunctionResolver, T as DagBuilder, U as AssemblyReader, V as resolveStateBucketWithDefaultAndSource, X as LocalInvokeBuildError, Z as LocalMigrateError, _ as normalizeAwsTagsToCfn, a as withRetry, at as RouteDiscoveryError, b as CloudControlProvider, c as cyan, d as red, et as MissingCdkCliError, f as yellow, ft as normalizeAwsError, g as matchesCdkPath, h as CDK_PATH_TAG, i as withResourceDeadline, it as ResourceUpdateNotSupportedError, j as stringifyValue, k as shouldRetainResource, l as gray, m as collectInlinePolicyNamesManagedBySiblings, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as ProvisioningError, o as IMPLICIT_DELETE_DEPENDENCIES, ot as StackHasActiveImportsError, p as IAMRoleProvider, pt as withErrorHandling, q as CdkdError, r as DeployEngine, rt as ResourceTimeoutError, s as bold, st as StackTerminationProtectionError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as PartialFailureError, u as green, v as resolveExplicitPhysicalId, w as DiffCalculator, x as assertRegionMatch, y as ProviderRegistry, z as resolveSkipPrefix } from "./deploy-engine-CX1x5ug1.js";
4
4
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-BF03Alpe.js";
5
5
  import { createHash, createHmac, createPublicKey, createVerify, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListBucketsCommand, ListDirectoryBucketsCommand, 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,7 +20,7 @@ import { CloudFrontClient, CreateCloudFrontOriginAccessIdentityCommand, CreateDi
20
20
  import { CloudWatchClient, DeleteAlarmsCommand, DescribeAlarmsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$4, PutMetricAlarmCommand, TagResourceCommand as TagResourceCommand$6, UntagResourceCommand as UntagResourceCommand$6 } from "@aws-sdk/client-cloudwatch";
21
21
  import { CloudWatchLogsClient, CreateLogGroupCommand, DeleteDataProtectionPolicyCommand, DeleteIndexPolicyCommand, DeleteLogGroupCommand, DeleteRetentionPolicyCommand, DescribeIndexPoliciesCommand, DescribeLogGroupsCommand, GetDataProtectionPolicyCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$5, PutBearerTokenAuthenticationCommand, PutDataProtectionPolicyCommand, PutIndexPolicyCommand, PutLogGroupDeletionProtectionCommand, PutRetentionPolicyCommand, ResourceAlreadyExistsException, ResourceNotFoundException as ResourceNotFoundException$4, TagResourceCommand as TagResourceCommand$7, UntagResourceCommand as UntagResourceCommand$7 } from "@aws-sdk/client-cloudwatch-logs";
22
22
  import { BedrockAgentCoreControlClient, CreateAgentRuntimeCommand, DeleteAgentRuntimeCommand, GetAgentRuntimeCommand, ResourceNotFoundException as ResourceNotFoundException$5, UpdateAgentRuntimeCommand } from "@aws-sdk/client-bedrock-agentcore-control";
23
- import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
23
+ import { cpSync, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
24
24
  import * as path from "node:path";
25
25
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
26
26
  import { execFile, spawn } from "node:child_process";
@@ -34750,6 +34750,31 @@ function createStateCommand() {
34750
34750
  return cmd;
34751
34751
  }
34752
34752
 
34753
+ //#endregion
34754
+ //#region src/cli/cfn-stack-states.ts
34755
+ /**
34756
+ * Stable terminal CloudFormation stack states.
34757
+ *
34758
+ * Sourced from the AWS CloudFormation API documentation: a stack in any
34759
+ * of these states has settled (success or rolled-back) and is safe to
34760
+ * read / mutate. Every other status (`*_IN_PROGRESS`, `*_FAILED`,
34761
+ * `REVIEW_IN_PROGRESS`) means the stack is mid-operation or in an
34762
+ * unhealthy state — callers gate AWS-side mutations behind this set so
34763
+ * the user can settle the source before paying for further work.
34764
+ *
34765
+ * Single source of truth — consumed by both `cdkd migrate`'s pre-flight
34766
+ * check (`src/cli/commands/migrate/cfn-stack-prefetch.ts`) and the
34767
+ * `cdkd import --migrate-from-cloudformation` retirement flow
34768
+ * (`src/cli/commands/retire-cfn-stack.ts`).
34769
+ */
34770
+ const STABLE_TERMINAL_STATUSES = new Set([
34771
+ "CREATE_COMPLETE",
34772
+ "UPDATE_COMPLETE",
34773
+ "UPDATE_ROLLBACK_COMPLETE",
34774
+ "IMPORT_COMPLETE",
34775
+ "IMPORT_ROLLBACK_COMPLETE"
34776
+ ]);
34777
+
34753
34778
  //#endregion
34754
34779
  //#region src/cli/upload-cfn-template.ts
34755
34780
  /**
@@ -35191,18 +35216,6 @@ function parseCfnTemplateWithFormat(text) {
35191
35216
  //#endregion
35192
35217
  //#region src/cli/commands/retire-cfn-stack.ts
35193
35218
  /**
35194
- * Stack states from which an UpdateStack call is safe. Anything else (an
35195
- * IN_PROGRESS, FAILED, or REVIEW_IN_PROGRESS state) means the stack is
35196
- * mid-operation or in an unhealthy state we should not touch.
35197
- */
35198
- const STABLE_TERMINAL_STATUSES = new Set([
35199
- "CREATE_COMPLETE",
35200
- "UPDATE_COMPLETE",
35201
- "UPDATE_ROLLBACK_COMPLETE",
35202
- "IMPORT_COMPLETE",
35203
- "IMPORT_ROLLBACK_COMPLETE"
35204
- ]);
35205
- /**
35206
35219
  * UpdateStack TemplateBody hard limit (51,200 bytes). Templates larger than
35207
35220
  * this are uploaded to cdkd's state S3 bucket and submitted via `TemplateURL`
35208
35221
  * instead — see {@link uploadTemplateForUpdateStack}.
@@ -35291,10 +35304,15 @@ async function retireCloudFormationStack(options) {
35291
35304
  logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
35292
35305
  let updateRan = false;
35293
35306
  try {
35307
+ const previousParameters = (stack.Parameters ?? []).map((p) => ({
35308
+ ParameterKey: p.ParameterKey,
35309
+ UsePreviousValue: true
35310
+ }));
35294
35311
  await cfnClient.send(new UpdateStackCommand({
35295
35312
  StackName: cfnStackName,
35296
35313
  ...updateInput,
35297
- Capabilities: capabilities
35314
+ Capabilities: capabilities,
35315
+ ...previousParameters.length > 0 && { Parameters: previousParameters }
35298
35316
  }));
35299
35317
  updateRan = true;
35300
35318
  } catch (err) {
@@ -35435,6 +35453,9 @@ async function confirmPrompt$2(prompt) {
35435
35453
 
35436
35454
  //#endregion
35437
35455
  //#region src/cli/commands/import.ts
35456
+ async function runImport(stackArg, options) {
35457
+ return importCommand(stackArg, options);
35458
+ }
35438
35459
  async function importCommand(stackArg, options) {
35439
35460
  const logger = getLogger();
35440
35461
  if (options.verbose) {
@@ -38748,7 +38769,7 @@ function resolveRuntimeCodeMountPath(runtime) {
38748
38769
 
38749
38770
  //#endregion
38750
38771
  //#region src/local/docker-runner.ts
38751
- const execFileAsync$2 = promisify(execFile);
38772
+ const execFileAsync$3 = promisify(execFile);
38752
38773
  /**
38753
38774
  * Wraps `docker pull` / `docker run` / `docker rm` for `cdkd local invoke`.
38754
38775
  *
@@ -38840,7 +38861,7 @@ async function runDetached(opts) {
38840
38861
  args.push(opts.image, ...entryPointTail, ...opts.cmd);
38841
38862
  getLogger().child("docker").debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
38842
38863
  try {
38843
- const { stdout } = await execFileAsync$2(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
38864
+ const { stdout } = await execFileAsync$3(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
38844
38865
  return stdout.trim();
38845
38866
  } catch (error) {
38846
38867
  const err = error;
@@ -38877,7 +38898,7 @@ async function removeContainer(containerId) {
38877
38898
  if (!containerId) return;
38878
38899
  const logger = getLogger().child("docker");
38879
38900
  try {
38880
- await execFileAsync$2(getDockerCmd(), [
38901
+ await execFileAsync$3(getDockerCmd(), [
38881
38902
  "rm",
38882
38903
  "-f",
38883
38904
  containerId
@@ -38896,7 +38917,7 @@ async function removeContainer(containerId) {
38896
38917
  async function ensureDockerAvailable() {
38897
38918
  const cmd = getDockerCmd();
38898
38919
  try {
38899
- await execFileAsync$2(cmd, [
38920
+ await execFileAsync$3(cmd, [
38900
38921
  "version",
38901
38922
  "--format",
38902
38923
  "{{.Server.Version}}"
@@ -48039,7 +48060,7 @@ function createLocalStartApiCommand() {
48039
48060
 
48040
48061
  //#endregion
48041
48062
  //#region src/local/ecs-network.ts
48042
- const execFileAsync$1 = promisify(execFile);
48063
+ const execFileAsync$2 = promisify(execFile);
48043
48064
  /**
48044
48065
  * Docker network + AWS-published metadata-endpoints sidecar lifecycle for
48045
48066
  * `cdkd local run-task`. The sidecar (a small Go binary maintained by
@@ -48102,7 +48123,7 @@ async function createTaskNetwork(options = {}) {
48102
48123
  await pullImage(METADATA_ENDPOINT_IMAGE, options.skipPull ?? false);
48103
48124
  logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
48104
48125
  try {
48105
- await execFileAsync$1(getDockerCmd(), [
48126
+ await execFileAsync$2(getDockerCmd(), [
48106
48127
  "network",
48107
48128
  "create",
48108
48129
  "--driver",
@@ -48138,7 +48159,7 @@ async function createTaskNetwork(options = {}) {
48138
48159
  logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
48139
48160
  let sidecarContainerId;
48140
48161
  try {
48141
- const { stdout } = await execFileAsync$1(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
48162
+ const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
48142
48163
  sidecarContainerId = stdout.trim();
48143
48164
  } catch (err) {
48144
48165
  await destroyNetworkOnly(networkName);
@@ -48186,7 +48207,7 @@ async function destroyNetworkOnly(networkName) {
48186
48207
  if (!networkName) return;
48187
48208
  const logger = getLogger().child("ecs-network");
48188
48209
  try {
48189
- await execFileAsync$1(getDockerCmd(), [
48210
+ await execFileAsync$2(getDockerCmd(), [
48190
48211
  "network",
48191
48212
  "rm",
48192
48213
  networkName
@@ -48319,7 +48340,7 @@ async function resolveSsm(entry, shape, client) {
48319
48340
 
48320
48341
  //#endregion
48321
48342
  //#region src/local/ecs-task-runner.ts
48322
- const execFileAsync = promisify(execFile);
48343
+ const execFileAsync$1 = promisify(execFile);
48323
48344
  /**
48324
48345
  * Top-level orchestrator for `cdkd local run-task`. Coordinates image
48325
48346
  * preparation, secret resolution, docker-network bring-up, container
@@ -48379,7 +48400,7 @@ async function cleanupEcsRun(state, options) {
48379
48400
  await destroyTaskNetwork(state.network);
48380
48401
  state.network = void 0;
48381
48402
  for (const v of state.dockerVolumeNames) try {
48382
- await execFileAsync(getDockerCmd(), [
48403
+ await execFileAsync$1(getDockerCmd(), [
48383
48404
  "volume",
48384
48405
  "rm",
48385
48406
  v
@@ -48445,7 +48466,7 @@ async function runEcsTask(task, options, state) {
48445
48466
  logger.info(`Starting container '${container.name}' (image=${imagePlan.get(container.name)})`);
48446
48467
  let id;
48447
48468
  try {
48448
- const { stdout } = await execFileAsync(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
48469
+ const { stdout } = await execFileAsync$1(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
48449
48470
  id = stdout.trim();
48450
48471
  } catch (err) {
48451
48472
  const e = err;
@@ -48554,7 +48575,7 @@ async function waitForContainerHealthy(containerId, displayName) {
48554
48575
  let lastStatus = "";
48555
48576
  while (Date.now() < deadline) {
48556
48577
  try {
48557
- const { stdout } = await execFileAsync(getDockerCmd(), [
48578
+ const { stdout } = await execFileAsync$1(getDockerCmd(), [
48558
48579
  "inspect",
48559
48580
  "--format",
48560
48581
  "{{.State.Health.Status}}",
@@ -48577,7 +48598,7 @@ async function waitForContainerHealthy(containerId, displayName) {
48577
48598
  }
48578
48599
  async function waitForContainerExit(containerId) {
48579
48600
  try {
48580
- const { stdout } = await execFileAsync(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
48601
+ const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
48581
48602
  const code = Number.parseInt(stdout.trim(), 10);
48582
48603
  return Number.isFinite(code) ? code : 1;
48583
48604
  } catch (err) {
@@ -48587,7 +48608,7 @@ async function waitForContainerExit(containerId) {
48587
48608
  }
48588
48609
  async function stopContainer(containerId, graceSeconds) {
48589
48610
  try {
48590
- await execFileAsync(getDockerCmd(), [
48611
+ await execFileAsync$1(getDockerCmd(), [
48591
48612
  "stop",
48592
48613
  "-t",
48593
48614
  String(graceSeconds),
@@ -48712,7 +48733,7 @@ async function realizeDockerVolumes(volumes, state) {
48712
48733
  const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
48713
48734
  args.push(dockerVolumeName);
48714
48735
  try {
48715
- await execFileAsync(getDockerCmd(), args);
48736
+ await execFileAsync$1(getDockerCmd(), args);
48716
48737
  state.dockerVolumeNames.push(dockerVolumeName);
48717
48738
  logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
48718
48739
  } catch (err) {
@@ -51846,6 +51867,1069 @@ function createExportCommand() {
51846
51867
  return cmd;
51847
51868
  }
51848
51869
 
51870
+ //#endregion
51871
+ //#region src/cli/commands/migrate/cdk-cli-check.ts
51872
+ const execFileAsync = promisify(execFile);
51873
+ /**
51874
+ * Minimum aws-cdk CLI version where `cdk migrate --from-stack` is
51875
+ * considered stable (per the design doc at docs/design/465-cfn-migrate.md
51876
+ * §4 — "Version requirement"). Below this we WARN but do not block,
51877
+ * because users may have a working older release and a hard rejection
51878
+ * would be more disruptive than a warning.
51879
+ */
51880
+ const RECOMMENDED_MIN_VERSION = "2.124.0";
51881
+ /**
51882
+ * Verify that the upstream `cdk` CLI is available on PATH (or at the
51883
+ * override path passed via `--cdk-bin`) and report its version.
51884
+ *
51885
+ * Hard-errors with {@link MissingCdkCliError} when `cdk` cannot be
51886
+ * spawned (ENOENT, permission denied, etc.). Emits a `warn` field on
51887
+ * the result when the version string is below the recommended minimum
51888
+ * — the caller decides how to surface it.
51889
+ *
51890
+ * `cdk --version` output format empirically (verified 2026-05-22
51891
+ * against cdk@2.1112.0): `<MAJOR.MINOR.PATCH> (build <id>)`. The
51892
+ * version is the first whitespace-separated token; the trailing
51893
+ * `(build <id>)` is informational and ignored.
51894
+ *
51895
+ * @param cdkBinPath - Path to the `cdk` binary. Defaults to `'cdk'`
51896
+ * which uses the system PATH for resolution.
51897
+ */
51898
+ async function verifyCdkCliAvailable(cdkBinPath = "cdk") {
51899
+ let stdout;
51900
+ try {
51901
+ stdout = (await execFileAsync(cdkBinPath, ["--version"])).stdout ?? "";
51902
+ } catch (err) {
51903
+ throw new MissingCdkCliError(`Failed to run '${cdkBinPath} --version': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
51904
+ }
51905
+ const version = parseCdkVersion(stdout);
51906
+ if (!version) throw new MissingCdkCliError(`'${cdkBinPath} --version' produced unexpected output: ${JSON.stringify(stdout.trim())}`);
51907
+ if (compareSemver(version, RECOMMENDED_MIN_VERSION) < 0) return {
51908
+ version,
51909
+ 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.`
51910
+ };
51911
+ return { version };
51912
+ }
51913
+ /**
51914
+ * Parse the first semver-shaped token (`<MAJOR>.<MINOR>.<PATCH>`) out of
51915
+ * `cdk --version` stdout. Tolerates a trailing `(build <id>)` suffix.
51916
+ *
51917
+ * Returns `undefined` when no semver-shaped token is present so the
51918
+ * caller can surface the raw stdout in the error message.
51919
+ */
51920
+ function parseCdkVersion(stdout) {
51921
+ const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
51922
+ if (!match) return void 0;
51923
+ return `${match[1]}.${match[2]}.${match[3]}`;
51924
+ }
51925
+ /**
51926
+ * Compare two semver strings (`MAJOR.MINOR.PATCH`). Returns a negative
51927
+ * number when `a < b`, zero when `a === b`, positive when `a > b`.
51928
+ *
51929
+ * Strictly numeric comparison per component — no pre-release / build
51930
+ * suffix handling needed for this use case (`cdk --version` always
51931
+ * emits a stable `X.Y.Z` triple).
51932
+ */
51933
+ function compareSemver(a, b) {
51934
+ const aParts = a.split(".").map((n) => Number.parseInt(n, 10));
51935
+ const bParts = b.split(".").map((n) => Number.parseInt(n, 10));
51936
+ for (let i = 0; i < 3; i++) {
51937
+ const aN = aParts[i] ?? 0;
51938
+ const bN = bParts[i] ?? 0;
51939
+ if (aN !== bN) return aN - bN;
51940
+ }
51941
+ return 0;
51942
+ }
51943
+
51944
+ //#endregion
51945
+ //#region src/cli/commands/migrate/cfn-stack-prefetch.ts
51946
+ /**
51947
+ * Resource types that `cdkd migrate` refuses to adopt under any
51948
+ * circumstances. The rationale per type:
51949
+ *
51950
+ * - `AWS::CloudFormation::CustomResource` — Lambda-backed Custom
51951
+ * Resources whose response protocol (cfn-response over a
51952
+ * pre-signed S3 URL) is incompatible with cdkd's import flow. The
51953
+ * backing Lambda's onCreate handler would need to be re-invoked
51954
+ * for a cdkd-side adoption to make sense, and that's structurally
51955
+ * different from the metadata-transfer migration this command
51956
+ * provides.
51957
+ *
51958
+ * - `AWS::CloudFormation::Stack` — nested stacks. cdkd has no
51959
+ * provider for this type, and the matching `cdk migrate` output
51960
+ * flattens nested stacks into separate generated apps in a way
51961
+ * that doesn't round-trip cleanly. Out of scope for #465.
51962
+ *
51963
+ * - `Custom::*` — any user-defined Custom Resource type prefix.
51964
+ * Same rationale as `AWS::CloudFormation::CustomResource`.
51965
+ */
51966
+ const HARD_REJECT_RESOURCE_TYPES = new Set(["AWS::CloudFormation::CustomResource", "AWS::CloudFormation::Stack"]);
51967
+ /**
51968
+ * Fetch the source CFn stack's current state + resources + transform
51969
+ * info. Issues 3 read-only AWS API calls (`DescribeStacks`,
51970
+ * `DescribeStackResources`, `GetTemplate(Stage=Original)`); none of
51971
+ * them mutate AWS state. The result is fed into
51972
+ * {@link validatePrefetchResult} for the hard-reject check and
51973
+ * surfaced to {@link runMigrateLibrary}'s caller (PR B) as part of
51974
+ * `RunMigrateLibraryResult`.
51975
+ *
51976
+ * `DescribeStackResources` is preferred over `ListStackResources`
51977
+ * because the response is unpaginated up to 500 resources (CFn's hard
51978
+ * stack cap) — no pagination loop needed for the migration use case.
51979
+ */
51980
+ async function prefetchCfnStack(stackName, cfnClient) {
51981
+ const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }))).Stacks?.[0];
51982
+ if (!stack) throw new LocalMigrateError(`CloudFormation stack '${stackName}' not found.`);
51983
+ const stackStatus = stack.StackStatus ?? "";
51984
+ const resourcesResp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: stackName }));
51985
+ const resources = [];
51986
+ for (const r of resourcesResp.StackResources ?? []) {
51987
+ if (!r.LogicalResourceId || !r.PhysicalResourceId || !r.ResourceType) continue;
51988
+ resources.push({
51989
+ LogicalResourceId: r.LogicalResourceId,
51990
+ PhysicalResourceId: r.PhysicalResourceId,
51991
+ ResourceType: r.ResourceType
51992
+ });
51993
+ }
51994
+ let transformInfo = {
51995
+ hasSamTransform: false,
51996
+ hasIncludeTransform: false
51997
+ };
51998
+ let sourceCfnTemplate = null;
51999
+ try {
52000
+ const tplResp = await cfnClient.send(new GetTemplateCommand({
52001
+ StackName: stackName,
52002
+ TemplateStage: "Original"
52003
+ }));
52004
+ if (tplResp.TemplateBody) {
52005
+ try {
52006
+ sourceCfnTemplate = parseCfnTemplate(tplResp.TemplateBody);
52007
+ } catch {
52008
+ sourceCfnTemplate = null;
52009
+ }
52010
+ transformInfo = detectTransformsFromParsed(sourceCfnTemplate);
52011
+ }
52012
+ } catch (err) {
52013
+ const detail = err instanceof Error ? err.message : String(err);
52014
+ getLogger().warn(`[migrate] GetTemplate failed for '${stackName}': ${detail}. Skipping transform detection.`);
52015
+ }
52016
+ return {
52017
+ stackStatus,
52018
+ resources,
52019
+ transformInfo,
52020
+ sourceCfnTemplate
52021
+ };
52022
+ }
52023
+ /**
52024
+ * Pre-flight reject when the source CFn stack contains any
52025
+ * un-migratable resource type OR is in a non-terminal state. Surfaces
52026
+ * SAM / Include transforms via the result's `transformInfo` instead —
52027
+ * the caller is responsible for emitting INFO logs.
52028
+ *
52029
+ * Per #465 Q2 (parent-session decision): every hard-reject case
52030
+ * routes the user to the standalone `cdk migrate --from-stack <name>
52031
+ * --output-dir ./tmp` flow so they can manually inspect the generated
52032
+ * code and decide how to proceed (e.g. delete the Custom Resource
52033
+ * from the source CFn stack before re-running cdkd migrate).
52034
+ */
52035
+ function validatePrefetchResult(result) {
52036
+ 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'.`);
52037
+ const offenders = [];
52038
+ for (const r of result.resources) if (isHardRejectType(r.ResourceType)) offenders.push({
52039
+ LogicalResourceId: r.LogicalResourceId,
52040
+ ResourceType: r.ResourceType
52041
+ });
52042
+ if (offenders.length === 0) return;
52043
+ 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.`);
52044
+ }
52045
+ /**
52046
+ * Whether the given resource type is in the hard-reject set. Matches
52047
+ * literal types from {@link HARD_REJECT_RESOURCE_TYPES} AND the
52048
+ * `Custom::*` prefix pattern (user-defined Custom Resource types).
52049
+ */
52050
+ function isHardRejectType(resourceType) {
52051
+ if (HARD_REJECT_RESOURCE_TYPES.has(resourceType)) return true;
52052
+ if (resourceType.startsWith("Custom::")) return true;
52053
+ return false;
52054
+ }
52055
+ /**
52056
+ * Scan an already-parsed CFn template object for stack-level
52057
+ * Transform blocks. Accepts both the scalar form (`Transform:
52058
+ * AWS::Serverless-2016-10-31`) and the array form (`Transform:
52059
+ * [AWS::Serverless-2016-10-31, ...]`).
52060
+ *
52061
+ * The caller is responsible for parsing the raw template body via the
52062
+ * CFn-aware codec; this helper operates on the parsed object so the
52063
+ * `GetTemplate` response can be parsed once and reused.
52064
+ */
52065
+ function detectTransformsFromParsed(parsed) {
52066
+ if (!parsed || typeof parsed !== "object") return {
52067
+ hasSamTransform: false,
52068
+ hasIncludeTransform: false
52069
+ };
52070
+ const transformRaw = parsed["Transform"];
52071
+ const transforms = Array.isArray(transformRaw) ? transformRaw.map((t) => String(t)) : typeof transformRaw === "string" ? [transformRaw] : [];
52072
+ return {
52073
+ hasSamTransform: transforms.some((t) => t.startsWith("AWS::Serverless")),
52074
+ hasIncludeTransform: transforms.some((t) => t === "AWS::Include" || t.startsWith("AWS::Include"))
52075
+ };
52076
+ }
52077
+
52078
+ //#endregion
52079
+ //#region src/cli/commands/migrate/output-dir-guard.ts
52080
+ /**
52081
+ * Refuse to run `cdkd migrate` when the upstream `cdk migrate` would
52082
+ * write its output into an existing non-empty directory. `cdk migrate`
52083
+ * itself silently overwrites in that case; cdkd surfaces the collision
52084
+ * up-front with the exact recovery command so the user is not left
52085
+ * with a half-overwritten CDK app.
52086
+ *
52087
+ * Behavior matrix (`outputPath = <user-supplied dir>`, `stackName =
52088
+ * <CFn stack name>`; cdk writes to `<outputPath>/<stackName>`):
52089
+ * - target dir does not exist → OK
52090
+ * - target dir exists but is empty → OK (treated as fresh)
52091
+ * - target path is a file, not a directory → typed error
52092
+ * - target dir exists and contains entries → typed error with
52093
+ * the canonical
52094
+ * `rm -rf` recovery
52095
+ * command in the
52096
+ * message
52097
+ *
52098
+ * The parent `outputPath` is NOT checked — `cdk migrate` creates
52099
+ * `<outputPath>` itself if missing, so a non-existent parent is fine.
52100
+ */
52101
+ function assertOutputDirAvailable(outputPath, stackName) {
52102
+ const targetDir = resolve(outputPath, stackName);
52103
+ if (!existsSync(targetDir)) return;
52104
+ 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}`);
52105
+ const entries = readdirSync(targetDir);
52106
+ if (entries.length === 0) return;
52107
+ 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}`);
52108
+ }
52109
+
52110
+ //#endregion
52111
+ //#region src/cli/commands/migrate/cdk-migrate-spawn.ts
52112
+ /**
52113
+ * Spawn `cdk migrate --from-stack` as a subprocess and stream its
52114
+ * output through cdkd's logger.
52115
+ *
52116
+ * Why subprocess (not Node module import): the upstream `cdk` CLI is
52117
+ * a bundled CommonJS app with no stable public API; pinning a
52118
+ * runtime-injected `aws-cdk-lib` from cdkd is impractical. Subprocess
52119
+ * isolation also gives us a clean failure boundary — non-zero exit
52120
+ * surfaces as {@link LocalMigrateError} with the captured streams
52121
+ * folded into the error message.
52122
+ *
52123
+ * Streaming is preferred over a buffered `execFile` because `cdk
52124
+ * migrate` emits progress lines (downloading the resource schema,
52125
+ * synthesizing the L1 code for each resource) that users expect to
52126
+ * see in real time. Captured streams are still returned for caller
52127
+ * inspection.
52128
+ */
52129
+ async function spawnCdkMigrate(opts) {
52130
+ const logger = getLogger();
52131
+ const cdkBin = opts.cdkBinPath ?? "cdk";
52132
+ const language = opts.language ?? "typescript";
52133
+ const outputDir = resolve(opts.outputPath, opts.stackName);
52134
+ const args = [
52135
+ "migrate",
52136
+ "--from-stack",
52137
+ "--stack-name",
52138
+ opts.fromStackName,
52139
+ "--output-path",
52140
+ opts.outputPath,
52141
+ "--language",
52142
+ language
52143
+ ];
52144
+ if (opts.region) args.push("--region", opts.region);
52145
+ if (opts.account) args.push("--account", opts.account);
52146
+ if (opts.profile) args.push("--profile", opts.profile);
52147
+ for (const filter of opts.filters ?? []) args.push("--filter", filter);
52148
+ const env = {
52149
+ ...process.env,
52150
+ ...opts.extraEnv ?? {}
52151
+ };
52152
+ logger.info(`[cdk-migrate] ${cdkBin} ${args.join(" ")}`);
52153
+ return await new Promise((resolvePromise, rejectPromise) => {
52154
+ let stdout = "";
52155
+ let stderr = "";
52156
+ let child;
52157
+ try {
52158
+ child = spawn(cdkBin, args, {
52159
+ env,
52160
+ stdio: [
52161
+ "ignore",
52162
+ "pipe",
52163
+ "pipe"
52164
+ ]
52165
+ });
52166
+ } catch (err) {
52167
+ rejectPromise(new LocalMigrateError(`Failed to spawn '${cdkBin} migrate': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
52168
+ return;
52169
+ }
52170
+ child.stdout?.on("data", (chunk) => {
52171
+ const text = chunk.toString("utf-8");
52172
+ stdout += text;
52173
+ const trimmed = text.replace(/\n$/, "");
52174
+ if (trimmed) logger.info(`[cdk-migrate] ${trimmed}`);
52175
+ });
52176
+ child.stderr?.on("data", (chunk) => {
52177
+ const text = chunk.toString("utf-8");
52178
+ stderr += text;
52179
+ const trimmed = text.replace(/\n$/, "");
52180
+ if (trimmed) logger.warn(`[cdk-migrate] ${trimmed}`);
52181
+ });
52182
+ child.on("error", (err) => {
52183
+ rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' subprocess error: ${err.message}`, err));
52184
+ });
52185
+ child.on("close", (code, signal) => {
52186
+ if (code === 0) {
52187
+ resolvePromise({
52188
+ outputDir,
52189
+ stdout,
52190
+ stderr
52191
+ });
52192
+ return;
52193
+ }
52194
+ rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' ${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`));
52195
+ });
52196
+ });
52197
+ }
52198
+
52199
+ //#endregion
52200
+ //#region src/cli/commands/migrate/synth-after-migrate.ts
52201
+ /**
52202
+ * Run `npm install` inside the generated CDK app directory so the
52203
+ * `cdk synth` step (which dynamically loads `aws-cdk-lib`) has its
52204
+ * dependencies in place.
52205
+ *
52206
+ * Skippable via `skipInstall: true` — CI users with a pre-populated
52207
+ * node_modules cache can save the ~30s `npm install` round-trip.
52208
+ *
52209
+ * Streams stdout / stderr through cdkd's logger so users see progress
52210
+ * for the often-slow npm step. Non-zero exit surfaces as a typed
52211
+ * {@link LocalMigrateError} with the captured streams folded into
52212
+ * the error message.
52213
+ */
52214
+ async function installGeneratedAppDeps(outputDir, opts) {
52215
+ const logger = getLogger();
52216
+ if (opts.skipInstall) {
52217
+ logger.info(`[cdk-migrate] Skipping 'npm install' (--skip-install).`);
52218
+ return;
52219
+ }
52220
+ const absDir = resolve(outputDir);
52221
+ if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'npm install'.`);
52222
+ logger.info(`[cdk-migrate] Running 'npm install' in ${absDir}...`);
52223
+ await runStreamingCommand("npm", ["install"], absDir, opts.extraEnv ? {
52224
+ ...process.env,
52225
+ ...opts.extraEnv
52226
+ } : void 0, "npm install");
52227
+ }
52228
+ /**
52229
+ * Run `cdk synth --quiet` inside the generated CDK app directory and
52230
+ * return both the assembly dir path and the parsed root-stack
52231
+ * template.
52232
+ *
52233
+ * `--quiet` suppresses the noisy template echo that `cdk synth`
52234
+ * defaults to on stdout — we read the template from disk
52235
+ * (`cdk.out/<StackName>.template.json`) instead.
52236
+ *
52237
+ * Skippable via `skipSynth: true` — useful when PR B's orchestrator
52238
+ * needs the codegen output without running synth (e.g. preview /
52239
+ * dry-run modes).
52240
+ */
52241
+ async function synthGeneratedApp(outputDir, opts) {
52242
+ const logger = getLogger();
52243
+ const absDir = resolve(outputDir);
52244
+ const assemblyDir = join(absDir, "cdk.out");
52245
+ if (opts.skipSynth) {
52246
+ logger.info(`[cdk-migrate] Skipping 'cdk synth' (--skip-synth).`);
52247
+ return {
52248
+ assemblyDir,
52249
+ templateBody: null
52250
+ };
52251
+ }
52252
+ if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'cdk synth'.`);
52253
+ const cdkBin = opts.cdkBinPath ?? "cdk";
52254
+ const env = {
52255
+ ...process.env,
52256
+ ...opts.extraEnv ?? {}
52257
+ };
52258
+ logger.info(`[cdk-migrate] Running '${cdkBin} synth --quiet' in ${absDir}...`);
52259
+ await runStreamingCommand(cdkBin, ["synth", "--quiet"], absDir, env, `${cdkBin} synth`);
52260
+ if (!existsSync(assemblyDir)) throw new LocalMigrateError(`'cdk synth' completed but produced no '${assemblyDir}' directory.`);
52261
+ const templates = readdirSync(assemblyDir).filter((f) => f.endsWith(".template.json"));
52262
+ if (templates.length === 0) return {
52263
+ assemblyDir,
52264
+ templateBody: null
52265
+ };
52266
+ const templatePath = join(assemblyDir, templates[0]);
52267
+ let templateBody;
52268
+ try {
52269
+ const raw = readFileSync(templatePath, "utf-8");
52270
+ templateBody = JSON.parse(raw);
52271
+ } catch (err) {
52272
+ throw new LocalMigrateError(`Failed to parse generated template '${templatePath}': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
52273
+ }
52274
+ return {
52275
+ assemblyDir,
52276
+ templateBody
52277
+ };
52278
+ }
52279
+ /**
52280
+ * Helper — spawn a subprocess in the given working directory, stream
52281
+ * its output through cdkd's logger, and reject with a typed
52282
+ * {@link LocalMigrateError} on non-zero exit.
52283
+ *
52284
+ * Factored out so the `npm install` and `cdk synth` call sites use the
52285
+ * same shape; both expose progress to the user and surface failures
52286
+ * with the captured streams.
52287
+ */
52288
+ async function runStreamingCommand(bin, args, cwd, env, label) {
52289
+ const logger = getLogger();
52290
+ return await new Promise((resolvePromise, rejectPromise) => {
52291
+ let stdout = "";
52292
+ let stderr = "";
52293
+ let child;
52294
+ try {
52295
+ child = spawn(bin, args, {
52296
+ cwd,
52297
+ env: env ?? process.env,
52298
+ stdio: [
52299
+ "ignore",
52300
+ "pipe",
52301
+ "pipe"
52302
+ ]
52303
+ });
52304
+ } catch (err) {
52305
+ rejectPromise(new LocalMigrateError(`Failed to spawn ${label}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
52306
+ return;
52307
+ }
52308
+ child.stdout?.on("data", (chunk) => {
52309
+ const text = chunk.toString("utf-8");
52310
+ stdout += text;
52311
+ const trimmed = text.replace(/\n$/, "");
52312
+ if (trimmed) logger.info(`[${label}] ${trimmed}`);
52313
+ });
52314
+ child.stderr?.on("data", (chunk) => {
52315
+ const text = chunk.toString("utf-8");
52316
+ stderr += text;
52317
+ const trimmed = text.replace(/\n$/, "");
52318
+ if (trimmed) logger.warn(`[${label}] ${trimmed}`);
52319
+ });
52320
+ child.on("error", (err) => {
52321
+ rejectPromise(new LocalMigrateError(`${label} subprocess error: ${err.message}`, err));
52322
+ });
52323
+ child.on("close", (code, signal) => {
52324
+ if (code === 0) {
52325
+ resolvePromise();
52326
+ return;
52327
+ }
52328
+ 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`));
52329
+ });
52330
+ });
52331
+ }
52332
+
52333
+ //#endregion
52334
+ //#region src/cli/commands/migrate/index.ts
52335
+ /**
52336
+ * Build an extra-env bag for subprocesses (`npm install`, `cdk synth`)
52337
+ * so they inherit the AWS profile the caller selected via `--profile`.
52338
+ *
52339
+ * Without this, `cdk synth` falls back to the SDK default credential
52340
+ * chain (or no credentials at all) when the user runs cdkd under a
52341
+ * non-default profile — which silently produces a template synthesized
52342
+ * against the wrong account / region context.
52343
+ */
52344
+ function buildAwsEnv(opts) {
52345
+ return opts.profile ? { AWS_PROFILE: opts.profile } : {};
52346
+ }
52347
+ /**
52348
+ * PR A library entry point — orchestrates the codegen + synth half of
52349
+ * `cdkd migrate --from-cfn-stack` without writing cdkd state, running
52350
+ * import, or touching the source CFn stack.
52351
+ *
52352
+ * Flow:
52353
+ * 1. `verifyCdkCliAvailable` — hard-fail if `cdk` is missing.
52354
+ * 2. `prefetchCfnStack` — read source stack state + resources +
52355
+ * transform info.
52356
+ * 3. `validatePrefetchResult` — reject CR / nested-stack / non-
52357
+ * terminal state; INFO log SAM / Include transforms.
52358
+ * 4. `assertOutputDirAvailable` — refuse pre-existing non-empty
52359
+ * output dir.
52360
+ * 5. `spawnCdkMigrate` — run `cdk migrate --from-stack` subprocess.
52361
+ * 6. `installGeneratedAppDeps` (gated by `skipInstall`).
52362
+ * 7. `synthGeneratedApp` (gated by `skipSynth`).
52363
+ * 8. Return every artifact PR B will consume.
52364
+ *
52365
+ * Per #465 Q4 (parent-session decision): PR A is library-only. The CLI
52366
+ * command (`cdkd migrate`), state writes, retire flow, and resource-
52367
+ * mapping algorithm all live in PR B.
52368
+ */
52369
+ async function runMigrateLibrary(opts) {
52370
+ const logger = getLogger();
52371
+ const stackName = opts.fromCfnStack;
52372
+ const outputPath = opts.outputDir ?? resolve(process.cwd(), stackName);
52373
+ logger.info(`[migrate] Verifying upstream 'cdk' CLI...`);
52374
+ const cliCheck = await verifyCdkCliAvailable(opts.cdkBinPath);
52375
+ logger.info(`[migrate] Using cdk CLI v${cliCheck.version}`);
52376
+ if (cliCheck.warn) logger.warn(`[migrate] ${cliCheck.warn}`);
52377
+ logger.info(`[migrate] Pre-fetching CloudFormation stack '${stackName}'...`);
52378
+ const cfnClient = buildCfnClient(opts);
52379
+ let prefetch;
52380
+ try {
52381
+ prefetch = await prefetchCfnStack(stackName, cfnClient);
52382
+ } finally {
52383
+ cfnClient.destroy?.();
52384
+ }
52385
+ validatePrefetchResult(prefetch);
52386
+ 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).");
52387
+ 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.");
52388
+ assertOutputDirAvailable(outputPath, stackName);
52389
+ const spawnResult = await spawnCdkMigrate({
52390
+ stackName,
52391
+ fromStackName: stackName,
52392
+ outputPath,
52393
+ ...opts.language && { language: opts.language },
52394
+ ...opts.region && { region: opts.region },
52395
+ ...opts.account && { account: opts.account },
52396
+ ...opts.filters && { filters: opts.filters },
52397
+ ...opts.profile && { profile: opts.profile },
52398
+ ...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath }
52399
+ });
52400
+ await installGeneratedAppDeps(spawnResult.outputDir, {
52401
+ skipInstall: opts.skipInstall ?? false,
52402
+ extraEnv: buildAwsEnv(opts)
52403
+ });
52404
+ const synthResult = await synthGeneratedApp(spawnResult.outputDir, {
52405
+ skipSynth: opts.skipSynth ?? false,
52406
+ ...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath },
52407
+ extraEnv: buildAwsEnv(opts)
52408
+ });
52409
+ 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.`);
52410
+ return {
52411
+ outputDir: spawnResult.outputDir,
52412
+ assemblyDir: synthResult.assemblyDir,
52413
+ templateBody: synthResult.templateBody,
52414
+ sourceCfnTemplate: prefetch.sourceCfnTemplate,
52415
+ sourceResources: prefetch.resources
52416
+ };
52417
+ }
52418
+ /**
52419
+ * Build a CloudFormation client using cdkd's shared {@link AwsClients}
52420
+ * factory so the credential chain matches every other cdkd command.
52421
+ *
52422
+ * Keeping the construction in one place makes the per-command CFn-
52423
+ * client recipe a single readable hook for future plumbing (e.g.
52424
+ * threading PR B's `--role-arn` STS-assume).
52425
+ */
52426
+ function buildCfnClient(opts) {
52427
+ const config = {};
52428
+ if (opts.region) config.region = opts.region;
52429
+ if (opts.profile) config.profile = opts.profile;
52430
+ return new AwsClients(config).getCloudFormationClient();
52431
+ }
52432
+
52433
+ //#endregion
52434
+ //#region src/cli/commands/migrate/resource-mapper.ts
52435
+ /**
52436
+ * Run the 2-pass mapping algorithm against `(sourceCfnTemplate,
52437
+ * synthTemplate, sourceResources)` and return the resolved mapping plus
52438
+ * any unmatched entries.
52439
+ *
52440
+ * Pure-functional: no AWS calls, no I/O, no mutation of inputs. Safe to
52441
+ * call repeatedly in tests against shared fixtures.
52442
+ *
52443
+ * Throws when an override references a synth id that does not exist —
52444
+ * this is a user error in the hand-edited mapping JSON and surfacing
52445
+ * the available synth ids in the message is the path back to a working
52446
+ * config.
52447
+ */
52448
+ function buildResourceMapping(opts) {
52449
+ const sourceEntries = extractSourceResources(opts.sourceCfnTemplate);
52450
+ const synthEntries = extractSynthResources(opts.synthTemplate);
52451
+ const physicalIdByLogicalId = /* @__PURE__ */ new Map();
52452
+ for (const r of opts.sourceResources) physicalIdByLogicalId.set(r.LogicalResourceId, r.PhysicalResourceId);
52453
+ const overrides = opts.overrides ?? {};
52454
+ const synthLogicalIds = new Set(synthEntries.map((s) => s.logicalId));
52455
+ 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(", ")}`);
52456
+ const missingPhysicalIds = [];
52457
+ for (const src of sourceEntries) if (!physicalIdByLogicalId.has(src.logicalId)) missingPhysicalIds.push(` - ${src.logicalId} (${src.type})`);
52458
+ 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.`);
52459
+ const synthByLastPathSegment = indexSynthByLastPathSegment(synthEntries);
52460
+ const synthByLogicalId = new Map(synthEntries.map((s) => [s.logicalId, s]));
52461
+ const claimedSynthIds = /* @__PURE__ */ new Set();
52462
+ const pairs = [];
52463
+ const unmatched = [];
52464
+ const sourcesHandledByOverride = /* @__PURE__ */ new Set();
52465
+ for (const [srcId, synthId] of Object.entries(overrides)) {
52466
+ const src = sourceEntries.find((e) => e.logicalId === srcId);
52467
+ if (!src) {
52468
+ const availableSrc = sourceEntries.map((e) => e.logicalId).sort();
52469
+ 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(", ")}`);
52470
+ }
52471
+ const synth = synthByLogicalId.get(synthId);
52472
+ if (!synth) continue;
52473
+ const physicalId = physicalIdByLogicalId.get(src.logicalId);
52474
+ pairs.push({
52475
+ sourceLogicalId: src.logicalId,
52476
+ synthLogicalId: synth.logicalId,
52477
+ physicalId,
52478
+ resourceType: src.type
52479
+ });
52480
+ claimedSynthIds.add(synth.logicalId);
52481
+ sourcesHandledByOverride.add(src.logicalId);
52482
+ }
52483
+ const passOnePending = [];
52484
+ for (const src of sourceEntries) {
52485
+ if (sourcesHandledByOverride.has(src.logicalId)) continue;
52486
+ const candidatesForLastSegment = synthByLastPathSegment.get(src.logicalId) ?? [];
52487
+ const rawCandidateCount = candidatesForLastSegment.length;
52488
+ const availableCandidates = candidatesForLastSegment.filter((c) => !claimedSynthIds.has(c.logicalId));
52489
+ if (rawCandidateCount === 1 && availableCandidates.length === 1) {
52490
+ const synth = availableCandidates[0];
52491
+ pairs.push({
52492
+ sourceLogicalId: src.logicalId,
52493
+ synthLogicalId: synth.logicalId,
52494
+ physicalId: physicalIdByLogicalId.get(src.logicalId),
52495
+ resourceType: src.type
52496
+ });
52497
+ claimedSynthIds.add(synth.logicalId);
52498
+ } else passOnePending.push(src);
52499
+ }
52500
+ for (const src of passOnePending) {
52501
+ const candidatesSameType = synthEntries.filter((s) => s.type === src.type && !claimedSynthIds.has(s.logicalId));
52502
+ const propertyMatches = candidatesSameType.filter((s) => deepEqualIgnoreNoValue(src.properties, s.properties));
52503
+ if (propertyMatches.length === 1) {
52504
+ const synth = propertyMatches[0];
52505
+ pairs.push({
52506
+ sourceLogicalId: src.logicalId,
52507
+ synthLogicalId: synth.logicalId,
52508
+ physicalId: physicalIdByLogicalId.get(src.logicalId),
52509
+ resourceType: src.type
52510
+ });
52511
+ claimedSynthIds.add(synth.logicalId);
52512
+ } else {
52513
+ const isCollision = (synthByLastPathSegment.get(src.logicalId) ?? []).length >= 2;
52514
+ unmatched.push({
52515
+ sourceLogicalId: src.logicalId,
52516
+ resourceType: src.type,
52517
+ candidates: candidatesSameType.map((c) => c.logicalId).sort(),
52518
+ reason: isCollision ? "logical-id-collision" : "no-match"
52519
+ });
52520
+ }
52521
+ }
52522
+ const mapping = {};
52523
+ for (const p of pairs) mapping[p.sourceLogicalId] = p.synthLogicalId;
52524
+ return {
52525
+ mapping,
52526
+ pairs,
52527
+ unmatched
52528
+ };
52529
+ }
52530
+ /**
52531
+ * Walk a parsed CFn template's `Resources` and return one entry per
52532
+ * resource. `Type` defaults to `''` and `Properties` to `{}` when the
52533
+ * template omits them (CFn allows resources with only `DeletionPolicy`
52534
+ * + `Type`); the mapper's downstream deep-equal handles `{}` cleanly.
52535
+ *
52536
+ * Top-level keys other than `Resources` (Conditions / Parameters /
52537
+ * Rules / Outputs / Mappings / Metadata) are NOT walked — the mapping
52538
+ * is resource-to-resource only, and §5.5 of the design doc spells out
52539
+ * the four CDK-synth injections that live in those sibling keys.
52540
+ */
52541
+ function extractSourceResources(template) {
52542
+ if (!template || typeof template !== "object") return [];
52543
+ const resources = template["Resources"];
52544
+ if (!resources || typeof resources !== "object") return [];
52545
+ const out = [];
52546
+ for (const [logicalId, raw] of Object.entries(resources)) {
52547
+ if (!raw || typeof raw !== "object") continue;
52548
+ const r = raw;
52549
+ const type = typeof r["Type"] === "string" ? r["Type"] : "";
52550
+ const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
52551
+ out.push({
52552
+ logicalId,
52553
+ type,
52554
+ properties: props
52555
+ });
52556
+ }
52557
+ return out;
52558
+ }
52559
+ /**
52560
+ * Walk the synth template's `Resources` and return one entry per
52561
+ * resource, EXCLUDING `AWS::CDK::Metadata` (synth-only sentinel that has
52562
+ * no source counterpart). Captures `Metadata['aws:cdk:path']` so Pass 1
52563
+ * can match against the last `/`-separated segment.
52564
+ *
52565
+ * The four other CDK-synth injections (CDKMetadataAvailable Condition,
52566
+ * BootstrapVersion Parameter, CheckBootstrapVersion Rule) live outside
52567
+ * `Resources` and are structurally excluded by this function.
52568
+ */
52569
+ function extractSynthResources(template) {
52570
+ if (!template || typeof template !== "object") return [];
52571
+ const resources = template["Resources"];
52572
+ if (!resources || typeof resources !== "object") return [];
52573
+ const out = [];
52574
+ for (const [logicalId, raw] of Object.entries(resources)) {
52575
+ if (!raw || typeof raw !== "object") continue;
52576
+ const r = raw;
52577
+ const type = typeof r["Type"] === "string" ? r["Type"] : "";
52578
+ if (type === "AWS::CDK::Metadata") continue;
52579
+ const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
52580
+ const meta = r["Metadata"] && typeof r["Metadata"] === "object" ? r["Metadata"] : {};
52581
+ const path = typeof meta["aws:cdk:path"] === "string" ? meta["aws:cdk:path"] : "";
52582
+ out.push({
52583
+ logicalId,
52584
+ type,
52585
+ properties: props,
52586
+ awsCdkPath: path
52587
+ });
52588
+ }
52589
+ return out;
52590
+ }
52591
+ /**
52592
+ * Group synth entries by the LAST `/`-separated segment of their
52593
+ * `aws:cdk:path` metadata so Pass 1 can look up the source logical id
52594
+ * directly. Per [docs/design/465-cfn-migrate.md](../../../../docs/design/465-cfn-migrate.md) §5.5:
52595
+ * `cdk migrate` emits `<StackName>/<LogicalId>` as the metadata value,
52596
+ * so the last segment IS the source logical id in the typical case.
52597
+ *
52598
+ * Entries without an `aws:cdk:path` are silently skipped — they cannot
52599
+ * be addressed by source logical id and will only be reachable via
52600
+ * Pass 2's Properties match.
52601
+ */
52602
+ function indexSynthByLastPathSegment(entries) {
52603
+ const out = /* @__PURE__ */ new Map();
52604
+ for (const e of entries) {
52605
+ if (!e.awsCdkPath) continue;
52606
+ const lastSegment = e.awsCdkPath.split("/").pop() ?? "";
52607
+ if (!lastSegment) continue;
52608
+ const bucket = out.get(lastSegment);
52609
+ if (bucket) bucket.push(e);
52610
+ else out.set(lastSegment, [e]);
52611
+ }
52612
+ return out;
52613
+ }
52614
+ /**
52615
+ * Recursive deep equality between two values, with two CFn-specific
52616
+ * accommodations:
52617
+ *
52618
+ * 1. Object key order is NOT significant — `JSON.stringify` would
52619
+ * diverge on the canonical-source-vs-synth-alphabetized case
52620
+ * (verified by PR A's empirical run, §5.5 point 3) so we walk the
52621
+ * keys explicitly.
52622
+ * 2. `AWS::NoValue` placeholder values are stripped before compare —
52623
+ * `cdk migrate`'s codegen sometimes emits the placeholder for a
52624
+ * property the source template simply omitted, and the structural
52625
+ * intent matches. Source side never has the placeholder; synth side
52626
+ * can on either side of a nested object.
52627
+ *
52628
+ * Arrays are compared positionally (order IS significant — Tags arrays
52629
+ * and similar carry ordering semantics).
52630
+ */
52631
+ function deepEqualIgnoreNoValue(a, b) {
52632
+ if (a === b) return true;
52633
+ if (isAwsNoValue(a) !== isAwsNoValue(b)) return false;
52634
+ if (isAwsNoValue(a) && isAwsNoValue(b)) return true;
52635
+ if (typeof a !== typeof b) return false;
52636
+ if (a === null || b === null) return a === b;
52637
+ if (typeof a !== "object") return a === b;
52638
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
52639
+ if (Array.isArray(a) && Array.isArray(b)) {
52640
+ if (a.length !== b.length) return false;
52641
+ for (let i = 0; i < a.length; i++) if (!deepEqualIgnoreNoValue(a[i], b[i])) return false;
52642
+ return true;
52643
+ }
52644
+ const aObj = a;
52645
+ const bObj = b;
52646
+ const aKeys = Object.keys(aObj).filter((k) => !isAwsNoValue(aObj[k]) && aObj[k] !== void 0);
52647
+ const bKeys = Object.keys(bObj).filter((k) => !isAwsNoValue(bObj[k]) && bObj[k] !== void 0);
52648
+ if (aKeys.length !== bKeys.length) return false;
52649
+ const aKeySet = new Set(aKeys);
52650
+ for (const k of bKeys) {
52651
+ if (!aKeySet.has(k)) return false;
52652
+ if (!deepEqualIgnoreNoValue(aObj[k], bObj[k])) return false;
52653
+ }
52654
+ return true;
52655
+ }
52656
+ /**
52657
+ * True when the value is the CFn `AWS::NoValue` placeholder
52658
+ * (`{Ref: 'AWS::NoValue'}`). `cdk migrate`'s codegen emits this for
52659
+ * conditional properties the source template omitted; the structural
52660
+ * intent matches and deep-equal must treat them as absent.
52661
+ */
52662
+ function isAwsNoValue(v) {
52663
+ if (!v || typeof v !== "object") return false;
52664
+ const obj = v;
52665
+ const keys = Object.keys(obj);
52666
+ return keys.length === 1 && keys[0] === "Ref" && obj["Ref"] === "AWS::NoValue";
52667
+ }
52668
+
52669
+ //#endregion
52670
+ //#region src/cli/commands/migrate/resource-mapping-file.ts
52671
+ /** Conventional filename inside the migrate output dir. */
52672
+ const RESOURCE_MAPPING_FILENAME = "cdkd-resource-mapping.json";
52673
+ /**
52674
+ * Build the on-disk file shape from a {@link ResourceMappingResult} +
52675
+ * the source / output stack names, then write it to
52676
+ * `<outputDir>/cdkd-resource-mapping.json` (overwriting any prior file
52677
+ * on the same path — same idempotency contract as `cdk migrate`'s own
52678
+ * codegen, and the user expects a re-run to refresh stale data).
52679
+ *
52680
+ * Returns the absolute on-disk path so the orchestrator can name it in
52681
+ * the error message when the mapping is incomplete.
52682
+ */
52683
+ function writeMappingFile(outputDir, args) {
52684
+ const path = join(outputDir, RESOURCE_MAPPING_FILENAME);
52685
+ const file = {
52686
+ version: 1,
52687
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
52688
+ sourceStack: args.sourceStack,
52689
+ outputStack: args.outputStack,
52690
+ mapping: args.result.mapping
52691
+ };
52692
+ if (args.result.unmatched.length > 0) file._unmatched = args.result.unmatched;
52693
+ writeFileSync(path, JSON.stringify(file, null, 2) + "\n", "utf-8");
52694
+ return path;
52695
+ }
52696
+ /**
52697
+ * Read a user-supplied resource-mapping file from disk and return the
52698
+ * `{<srcLogicalId>: <synthLogicalId>}` overrides for the mapper.
52699
+ *
52700
+ * Tolerates `_unmatched` on input (a user that re-runs against a
52701
+ * partial-failure file without editing should hit the same failure on
52702
+ * the resolution side — not a parse error here). Hard-errors on
52703
+ * structurally invalid input (missing `mapping`, non-object root,
52704
+ * non-string values, wrong `version`).
52705
+ *
52706
+ * Surfaces every shape error as `LocalMigrateError` (exit code 2) so
52707
+ * the CLI handler routes it through the normal error path.
52708
+ */
52709
+ function readMappingFile(path) {
52710
+ if (!existsSync(path)) throw new LocalMigrateError(`Resource-mapping file not found: ${path}. Drop the --resource-mapping flag or supply a valid path.`);
52711
+ let raw;
52712
+ try {
52713
+ raw = JSON.parse(readFileSync(path, "utf-8"));
52714
+ } catch (err) {
52715
+ throw new LocalMigrateError(`Resource-mapping file '${path}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
52716
+ }
52717
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new LocalMigrateError(`Resource-mapping file '${path}' must contain a JSON object at the top level.`);
52718
+ const obj = raw;
52719
+ if (obj["version"] !== 1) throw new LocalMigrateError(`Resource-mapping file '${path}' has unsupported version '${String(obj["version"])}'. Expected version 1.`);
52720
+ if (typeof obj["sourceStack"] !== "string" || obj["sourceStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'sourceStack' field.`);
52721
+ if (typeof obj["outputStack"] !== "string" || obj["outputStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'outputStack' field.`);
52722
+ if (typeof obj["generatedAt"] !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'generatedAt' field.`);
52723
+ const mappingRaw = obj["mapping"];
52724
+ if (!mappingRaw || typeof mappingRaw !== "object" || Array.isArray(mappingRaw)) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'mapping' object.`);
52725
+ const mapping = {};
52726
+ for (const [k, v] of Object.entries(mappingRaw)) {
52727
+ if (typeof v !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' has a non-string value for key '${k}'.`);
52728
+ mapping[k] = v;
52729
+ }
52730
+ const result = {
52731
+ version: 1,
52732
+ generatedAt: obj["generatedAt"],
52733
+ sourceStack: obj["sourceStack"],
52734
+ outputStack: obj["outputStack"],
52735
+ mapping
52736
+ };
52737
+ if (Array.isArray(obj["_unmatched"])) {
52738
+ const cleaned = [];
52739
+ for (const entry of obj["_unmatched"]) {
52740
+ if (!entry || typeof entry !== "object") continue;
52741
+ const e = entry;
52742
+ if (typeof e["sourceLogicalId"] !== "string" || typeof e["resourceType"] !== "string" || !Array.isArray(e["candidates"]) || e["reason"] !== "no-match" && e["reason"] !== "logical-id-collision") continue;
52743
+ cleaned.push({
52744
+ sourceLogicalId: e["sourceLogicalId"],
52745
+ resourceType: e["resourceType"],
52746
+ candidates: e["candidates"].filter((c) => typeof c === "string"),
52747
+ reason: e["reason"]
52748
+ });
52749
+ }
52750
+ if (cleaned.length > 0) result._unmatched = cleaned;
52751
+ }
52752
+ return result;
52753
+ }
52754
+
52755
+ //#endregion
52756
+ //#region src/cli/commands/migrate-command.ts
52757
+ /**
52758
+ * `cdkd migrate --from-cfn-stack <name>` end-to-end orchestrator.
52759
+ *
52760
+ * Closes the CLI half of [#465](https://github.com/go-to-k/cdkd/issues/465).
52761
+ * Reads the source CFn stack, runs upstream `cdk migrate` via the PR A
52762
+ * library, builds the source → synth logical-ID mapping with the 2-pass
52763
+ * algorithm, writes a `cdkd-resource-mapping.json` audit file, prompts
52764
+ * for confirmation, invokes `cdkd import` to write state under the
52765
+ * synth logical IDs, and (when `--retire-cfn-stack` is set) finally
52766
+ * retires the source CFn stack so management responsibility transfers
52767
+ * fully to cdkd.
52768
+ *
52769
+ * AWS resources are never modified — the migration is a metadata
52770
+ * transfer only. See [docs/design/465-cfn-migrate.md](../../../docs/design/465-cfn-migrate.md) §7
52771
+ * for the post-migration state matrix.
52772
+ */
52773
+ async function migrateCommandAction(positionalStack, options) {
52774
+ const logger = getLogger();
52775
+ if (options.verbose) {
52776
+ logger.setLevel("debug");
52777
+ process.env["CDKD_NO_LIVE"] = "1";
52778
+ }
52779
+ const sourceCfnStackName = options.fromCfnStack ?? positionalStack;
52780
+ if (!sourceCfnStackName) throw new LocalMigrateError("Missing required argument: --from-cfn-stack <name> (or pass the stack name positionally).");
52781
+ 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.");
52782
+ 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).");
52783
+ 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.");
52784
+ await applyRoleArnIfSet({
52785
+ roleArn: options.roleArn,
52786
+ region: options.region
52787
+ });
52788
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
52789
+ const outputDir = resolve(options.outputDir ?? sourceCfnStackName);
52790
+ logger.info(`[migrate] Source CFn stack: ${sourceCfnStackName}`);
52791
+ logger.info(`[migrate] Output directory: ${outputDir}`);
52792
+ const libResult = await runMigrateLibrary({
52793
+ fromCfnStack: sourceCfnStackName,
52794
+ outputDir,
52795
+ language: options.language ?? "typescript",
52796
+ ...options.region && { region: options.region },
52797
+ ...options.account && { account: options.account },
52798
+ ...options.filter && options.filter.length > 0 && { filters: options.filter },
52799
+ ...options.profile && { profile: options.profile },
52800
+ ...options.cdkBin && { cdkBinPath: options.cdkBin },
52801
+ skipInstall: options.skipInstall ?? false,
52802
+ skipSynth: options.skipSynth ?? false
52803
+ });
52804
+ if (options.skipSynth || !libResult.templateBody) {
52805
+ logger.info(`[migrate] --skip-synth: generated CDK app at ${libResult.outputDir}. Re-run without --skip-synth to write cdkd state.`);
52806
+ return;
52807
+ }
52808
+ let overrides;
52809
+ if (options.resourceMapping) {
52810
+ const mappingPath = resolve(options.resourceMapping);
52811
+ logger.info(`[migrate] Loading user-supplied resource mapping from ${mappingPath}`);
52812
+ overrides = readMappingFile(mappingPath).mapping;
52813
+ }
52814
+ let mappingResult;
52815
+ try {
52816
+ mappingResult = buildResourceMapping({
52817
+ sourceCfnTemplate: libResult.sourceCfnTemplate,
52818
+ synthTemplate: libResult.templateBody,
52819
+ sourceResources: libResult.sourceResources,
52820
+ ...overrides && { overrides }
52821
+ });
52822
+ } catch (err) {
52823
+ throw new LocalMigrateError(err instanceof Error ? err.message : String(err));
52824
+ }
52825
+ const outputStack = sourceCfnStackName;
52826
+ const mappingFilePath = writeMappingFile(libResult.outputDir, {
52827
+ sourceStack: sourceCfnStackName,
52828
+ outputStack,
52829
+ result: mappingResult
52830
+ });
52831
+ logger.info(`[migrate] Resource mapping written to ${mappingFilePath}`);
52832
+ if (mappingResult.unmatched.length > 0) {
52833
+ const lines = mappingResult.unmatched.map((u) => {
52834
+ const candidatesStr = u.candidates.length > 0 ? ` (synth candidates of same Type: ${u.candidates.join(", ")})` : " (no synth resource has the same Type)";
52835
+ return ` - ${u.sourceLogicalId} (${u.resourceType}) [${u.reason}]${candidatesStr}`;
52836
+ });
52837
+ 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.)`);
52838
+ }
52839
+ printMappingTable(mappingResult, sourceCfnStackName, outputStack);
52840
+ if (options.dryRun) {
52841
+ logger.info(`[migrate] --dry-run: would import ${mappingResult.pairs.length} resource(s) into cdkd state under stack '${outputStack}' (region '${region}'). State NOT written.`);
52842
+ return;
52843
+ }
52844
+ if (!options.yes) {
52845
+ if (!await promptConfirm(`Import ${mappingResult.pairs.length} resource(s) into cdkd state for stack '${outputStack}' (${region})?`)) {
52846
+ logger.info("[migrate] Cancelled by user. No state written.");
52847
+ return;
52848
+ }
52849
+ }
52850
+ const importMapping = {};
52851
+ for (const p of mappingResult.pairs) importMapping[p.synthLogicalId] = p.physicalId;
52852
+ const resolvedStateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
52853
+ await runImport(outputStack, {
52854
+ app: libResult.assemblyDir,
52855
+ statePrefix: options.statePrefix ?? "cdkd",
52856
+ resourceMappingInline: JSON.stringify(importMapping),
52857
+ auto: false,
52858
+ dryRun: false,
52859
+ yes: true,
52860
+ force: false,
52861
+ verbose: options.verbose ?? false,
52862
+ stateBucket: resolvedStateBucket,
52863
+ ...options.region && { region: options.region },
52864
+ ...options.profile && { profile: options.profile },
52865
+ ...options.roleArn && { roleArn: options.roleArn }
52866
+ });
52867
+ if (options.retireCfnStack) {
52868
+ logger.info(`[migrate] Retiring source CloudFormation stack '${sourceCfnStackName}'...`);
52869
+ const cfnConfig = {};
52870
+ if (options.region) cfnConfig.region = options.region;
52871
+ if (options.profile) cfnConfig.profile = options.profile;
52872
+ const awsClients = new AwsClients(cfnConfig);
52873
+ const cfnClient = awsClients.cloudFormation;
52874
+ try {
52875
+ await retireCloudFormationStack({
52876
+ cfnStackName: sourceCfnStackName,
52877
+ cfnClient,
52878
+ yes: options.yes ?? false,
52879
+ stateBucket: resolvedStateBucket,
52880
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
52881
+ });
52882
+ } finally {
52883
+ awsClients.destroy();
52884
+ }
52885
+ }
52886
+ logger.info(`[migrate] Migrated ${mappingResult.pairs.length} resource(s) from CloudFormation stack '${sourceCfnStackName}' to cdkd state for stack '${outputStack}'.`);
52887
+ logger.info(`[migrate] Generated CDK app at ${libResult.outputDir}`);
52888
+ 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).`);
52889
+ }
52890
+ /**
52891
+ * Print a tabular `(sourceLogicalId → synthLogicalId physicalId)`
52892
+ * summary so users can see what's about to be imported before the
52893
+ * confirmation prompt. Matches the visual shape of `cdkd import`'s
52894
+ * summary table for consistency.
52895
+ */
52896
+ function printMappingTable(result, sourceStack, outputStack) {
52897
+ const logger = getLogger();
52898
+ logger.info("");
52899
+ logger.info(`[migrate] Resolved mapping (${sourceStack} → ${outputStack}):`);
52900
+ for (const p of result.pairs) logger.info(` ${p.sourceLogicalId} → ${p.synthLogicalId} [${p.resourceType}] ${p.physicalId}`);
52901
+ logger.info("");
52902
+ }
52903
+ /**
52904
+ * Minimal `(y/N)` confirmation prompt using `node:readline`. Mirrors
52905
+ * the shape of [src/cli/commands/import.ts](import.ts)'s `confirmPrompt`
52906
+ * so the user UX is consistent across the import + migrate surfaces.
52907
+ * Non-TTY callers (CI) should pass `--yes` to skip this entirely.
52908
+ */
52909
+ async function promptConfirm(message) {
52910
+ if (!process.stdin.isTTY) throw new LocalMigrateError(`Non-interactive shell detected and --yes was not supplied. Re-run with --yes to confirm: "${message}"`);
52911
+ const rl = (await import("node:readline/promises")).createInterface({
52912
+ input: process.stdin,
52913
+ output: process.stdout
52914
+ });
52915
+ try {
52916
+ const v = (await rl.question(`${message} (y/N) `)).trim().toLowerCase();
52917
+ return v === "y" || v === "yes";
52918
+ } finally {
52919
+ rl.close();
52920
+ }
52921
+ }
52922
+ /**
52923
+ * Commander factory for `cdkd migrate`. Registered in `src/cli/index.ts`
52924
+ * alongside the existing top-level commands. Matches the design doc §3
52925
+ * flag set; the action handler routes through {@link migrateCommandAction}
52926
+ * via `withErrorHandling` so library callers see exceptions but CLI
52927
+ * callers get the standard exit-code-2 routing.
52928
+ */
52929
+ function createMigrateCommand() {
52930
+ 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("cdkd")).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));
52931
+ }
52932
+
51849
52933
  //#endregion
51850
52934
  //#region src/cli/index.ts
51851
52935
  const SUBCOMMANDS = new Set([
@@ -51863,7 +52947,8 @@ const SUBCOMMANDS = new Set([
51863
52947
  "publish-assets",
51864
52948
  "force-unlock",
51865
52949
  "state",
51866
- "local"
52950
+ "local",
52951
+ "migrate"
51867
52952
  ]);
51868
52953
  /**
51869
52954
  * Reorder args so options before the subcommand are moved after it.
@@ -51887,7 +52972,7 @@ function reorderArgs(argv) {
51887
52972
  */
51888
52973
  async function main() {
51889
52974
  const program = new Command();
51890
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.133.0");
52975
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.135.0");
51891
52976
  program.addCommand(createBootstrapCommand());
51892
52977
  program.addCommand(createSynthCommand());
51893
52978
  program.addCommand(createListCommand());
@@ -51902,6 +52987,7 @@ async function main() {
51902
52987
  program.addCommand(createStateCommand());
51903
52988
  program.addCommand(createLocalCommand());
51904
52989
  program.addCommand(createExportCommand());
52990
+ program.addCommand(createMigrateCommand());
51905
52991
  const args = reorderArgs(process.argv);
51906
52992
  await program.parseAsync(args);
51907
52993
  }