@go-to-k/cdkd 0.134.0 → 0.135.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -35304,10 +35304,15 @@ async function retireCloudFormationStack(options) {
35304
35304
  logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
35305
35305
  let updateRan = false;
35306
35306
  try {
35307
+ const previousParameters = (stack.Parameters ?? []).map((p) => ({
35308
+ ParameterKey: p.ParameterKey,
35309
+ UsePreviousValue: true
35310
+ }));
35307
35311
  await cfnClient.send(new UpdateStackCommand({
35308
35312
  StackName: cfnStackName,
35309
35313
  ...updateInput,
35310
- Capabilities: capabilities
35314
+ Capabilities: capabilities,
35315
+ ...previousParameters.length > 0 && { Parameters: previousParameters }
35311
35316
  }));
35312
35317
  updateRan = true;
35313
35318
  } catch (err) {
@@ -35448,6 +35453,9 @@ async function confirmPrompt$2(prompt) {
35448
35453
 
35449
35454
  //#endregion
35450
35455
  //#region src/cli/commands/import.ts
35456
+ async function runImport(stackArg, options) {
35457
+ return importCommand(stackArg, options);
35458
+ }
35451
35459
  async function importCommand(stackArg, options) {
35452
35460
  const logger = getLogger();
35453
35461
  if (options.verbose) {
@@ -38761,7 +38769,7 @@ function resolveRuntimeCodeMountPath(runtime) {
38761
38769
 
38762
38770
  //#endregion
38763
38771
  //#region src/local/docker-runner.ts
38764
- const execFileAsync$2 = promisify(execFile);
38772
+ const execFileAsync$3 = promisify(execFile);
38765
38773
  /**
38766
38774
  * Wraps `docker pull` / `docker run` / `docker rm` for `cdkd local invoke`.
38767
38775
  *
@@ -38853,7 +38861,7 @@ async function runDetached(opts) {
38853
38861
  args.push(opts.image, ...entryPointTail, ...opts.cmd);
38854
38862
  getLogger().child("docker").debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
38855
38863
  try {
38856
- const { stdout } = await execFileAsync$2(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
38864
+ const { stdout } = await execFileAsync$3(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
38857
38865
  return stdout.trim();
38858
38866
  } catch (error) {
38859
38867
  const err = error;
@@ -38890,7 +38898,7 @@ async function removeContainer(containerId) {
38890
38898
  if (!containerId) return;
38891
38899
  const logger = getLogger().child("docker");
38892
38900
  try {
38893
- await execFileAsync$2(getDockerCmd(), [
38901
+ await execFileAsync$3(getDockerCmd(), [
38894
38902
  "rm",
38895
38903
  "-f",
38896
38904
  containerId
@@ -38909,7 +38917,7 @@ async function removeContainer(containerId) {
38909
38917
  async function ensureDockerAvailable() {
38910
38918
  const cmd = getDockerCmd();
38911
38919
  try {
38912
- await execFileAsync$2(cmd, [
38920
+ await execFileAsync$3(cmd, [
38913
38921
  "version",
38914
38922
  "--format",
38915
38923
  "{{.Server.Version}}"
@@ -42004,29 +42012,50 @@ function selectIntegrationResponse(entries, matchTarget, fallbackStatusCode = 20
42004
42012
  try {
42005
42013
  if (new RegExp(`^${entry.SelectionPattern}$`).test(matchTarget)) return {
42006
42014
  entry,
42007
- statusCode: parseStatus$1(entry.StatusCode, fallbackStatusCode)
42015
+ statusCode: parseStatus(entry.StatusCode, fallbackStatusCode)
42008
42016
  };
42009
42017
  } catch {}
42010
42018
  }
42011
42019
  const entry = defaultEntry ?? null;
42012
42020
  return {
42013
42021
  entry,
42014
- statusCode: entry !== null ? parseStatus$1(entry.StatusCode, fallbackStatusCode) : fallbackStatusCode
42022
+ statusCode: entry !== null ? parseStatus(entry.StatusCode, fallbackStatusCode) : fallbackStatusCode
42015
42023
  };
42016
42024
  }
42017
- function parseStatus$1(raw, fallback) {
42025
+ /**
42026
+ * Parse an `IntegrationResponse.StatusCode` value into an HTTP status
42027
+ * code, returning `undefined` when the value is malformed or outside
42028
+ * the valid `[100, 600)` range.
42029
+ *
42030
+ * Issue (#507) items 6 + 7 + PR #515 item 4: this is the single source
42031
+ * of truth for "is `raw` a usable HTTP status code?" — `parseStatus`
42032
+ * in `rest-v1-integrations.ts` and this module's `parseStatus(raw, fallback)`
42033
+ * both delegate here so future shape-tightening (e.g. accepting `0x10`
42034
+ * the way `Number("0x10") === 16` does) lands in one place.
42035
+ *
42036
+ * Validation rules:
42037
+ * - non-string non-number → undefined
42038
+ * - empty / whitespace-only string → undefined
42039
+ * - NaN / non-integer → undefined
42040
+ * - integer outside `[100, 600)` (HTTP valid range) → undefined
42041
+ * - hex literal `"0x10"` → Number coerces to 16 but below 100 → undefined
42042
+ */
42043
+ function tryParseStatus(raw) {
42018
42044
  if (typeof raw === "number") {
42019
42045
  if (Number.isInteger(raw) && raw >= 100 && raw < 600) return raw;
42020
- return fallback;
42046
+ return;
42021
42047
  }
42022
- if (typeof raw !== "string") return fallback;
42048
+ if (typeof raw !== "string") return void 0;
42023
42049
  const trimmed = raw.trim();
42024
- if (trimmed === "") return fallback;
42050
+ if (trimmed === "") return void 0;
42025
42051
  const parsed = Number(trimmed);
42026
- if (!Number.isInteger(parsed)) return fallback;
42027
- if (parsed < 100 || parsed >= 600) return fallback;
42052
+ if (!Number.isInteger(parsed)) return void 0;
42053
+ if (parsed < 100 || parsed >= 600) return void 0;
42028
42054
  return parsed;
42029
42055
  }
42056
+ function parseStatus(raw, fallback) {
42057
+ return tryParseStatus(raw) ?? fallback;
42058
+ }
42030
42059
  /**
42031
42060
  * Evaluate `IntegrationResponse.ResponseParameters` — header literals
42032
42061
  * mapped onto the HTTP response. Returns `{name: value}` for every entry
@@ -42132,7 +42161,7 @@ function dispatchMockIntegration(config, req) {
42132
42161
  return vtlFailure("request", err, config.requestTemplate);
42133
42162
  }
42134
42163
  let entry = null;
42135
- if (pickedStatus !== void 0) entry = config.responses.find((e) => parseStatus(e.StatusCode) === pickedStatus) ?? defaultResponseEntry(config.responses);
42164
+ if (pickedStatus !== void 0) entry = config.responses.find((e) => tryParseStatus(e.StatusCode) === pickedStatus) ?? defaultResponseEntry(config.responses);
42136
42165
  else entry = defaultResponseEntry(config.responses);
42137
42166
  if (!entry) return {
42138
42167
  statusCode: pickedStatus ?? 200,
@@ -42156,7 +42185,7 @@ function dispatchMockIntegration(config, req) {
42156
42185
  Object.assign(headers, evaluateResponseParameters(entry.ResponseParameters, { onUnsupported: (_k, _v, reason) => logger.warn(`MOCK response: ${reason}`) }));
42157
42186
  if (body === "" && headers["content-type"] === contentType) delete headers["content-type"];
42158
42187
  return {
42159
- statusCode: parseStatus(entry.StatusCode) ?? 200,
42188
+ statusCode: tryParseStatus(entry.StatusCode) ?? 200,
42160
42189
  headers,
42161
42190
  body
42162
42191
  };
@@ -42461,19 +42490,6 @@ function extractStatusCodeFromRendered(rendered) {
42461
42490
  function defaultResponseEntry(entries) {
42462
42491
  return entries.find((e) => e.SelectionPattern === void 0 || e.SelectionPattern === "") ?? null;
42463
42492
  }
42464
- function parseStatus(raw) {
42465
- if (typeof raw === "number") {
42466
- if (Number.isInteger(raw) && raw >= 100 && raw < 600) return raw;
42467
- return;
42468
- }
42469
- if (typeof raw !== "string") return void 0;
42470
- const trimmed = raw.trim();
42471
- if (trimmed === "") return void 0;
42472
- const n = Number(trimmed);
42473
- if (!Number.isInteger(n)) return void 0;
42474
- if (n < 100 || n >= 600) return void 0;
42475
- return n;
42476
- }
42477
42493
  /**
42478
42494
  * Heuristic: is the given HTTP `Content-Type` header value likely to
42479
42495
  * carry text content that VTL ResponseTemplates can safely render
@@ -43781,6 +43797,38 @@ function matchHeaderList(headerList, allowed) {
43781
43797
  return true;
43782
43798
  }
43783
43799
 
43800
+ //#endregion
43801
+ //#region src/local/authorizer-context.ts
43802
+ /**
43803
+ * Build the per-kind context shape for the authorizer pipeline. Returns
43804
+ * an empty object only when nothing in the result is surfaceable (e.g.
43805
+ * a Lambda authorizer with no `principalId` and no `context`); callers
43806
+ * decide whether to skip surfacing in that case.
43807
+ *
43808
+ * For Lambda kinds: callers may need to wrap the returned shape in a
43809
+ * `lambda` namespace for HTTP API v2 (`{ lambda: shape }`); the wrap
43810
+ * is consumer-specific so it's NOT done here. The shape returned is
43811
+ * the always-flat REST v1 form.
43812
+ */
43813
+ function buildAuthorizerContextShape(authorizer, result) {
43814
+ if (authorizer.kind === "lambda-token" || authorizer.kind === "lambda-request") {
43815
+ const ctx = {};
43816
+ if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
43817
+ if (result.context) Object.assign(ctx, result.context);
43818
+ return ctx;
43819
+ }
43820
+ if (authorizer.kind === "iam") {
43821
+ const ctx = {};
43822
+ if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
43823
+ return ctx;
43824
+ }
43825
+ if (authorizer.kind === "cognito") return { claims: { ...result.context ?? {} } };
43826
+ return { jwt: {
43827
+ claims: { ...result.context ?? {} },
43828
+ scopes: []
43829
+ } };
43830
+ }
43831
+
43784
43832
  //#endregion
43785
43833
  //#region src/local/authorizer-resolver.ts
43786
43834
  /**
@@ -46292,22 +46340,7 @@ function buildServiceIntegrationContextVars(req, route) {
46292
46340
  */
46293
46341
  function buildAuthorizerContextForServiceIntegration(authorizer, result) {
46294
46342
  if (!authorizer || !result) return void 0;
46295
- if (authorizer.kind === "lambda-token" || authorizer.kind === "lambda-request") {
46296
- const ctx = {};
46297
- if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
46298
- if (result.context) Object.assign(ctx, result.context);
46299
- return ctx;
46300
- }
46301
- if (authorizer.kind === "iam") {
46302
- const ctx = {};
46303
- if (result.principalId !== void 0) ctx["principalId"] = result.principalId;
46304
- return ctx;
46305
- }
46306
- if (authorizer.kind === "cognito") return { claims: { ...result.context ?? {} } };
46307
- return { jwt: {
46308
- claims: { ...result.context ?? {} },
46309
- scopes: []
46310
- } };
46343
+ return buildAuthorizerContextShape(authorizer, result);
46311
46344
  }
46312
46345
  /**
46313
46346
  * Write the 501 Not Implemented response surfaced for routes the
@@ -48052,7 +48085,7 @@ function createLocalStartApiCommand() {
48052
48085
 
48053
48086
  //#endregion
48054
48087
  //#region src/local/ecs-network.ts
48055
- const execFileAsync$1 = promisify(execFile);
48088
+ const execFileAsync$2 = promisify(execFile);
48056
48089
  /**
48057
48090
  * Docker network + AWS-published metadata-endpoints sidecar lifecycle for
48058
48091
  * `cdkd local run-task`. The sidecar (a small Go binary maintained by
@@ -48115,7 +48148,7 @@ async function createTaskNetwork(options = {}) {
48115
48148
  await pullImage(METADATA_ENDPOINT_IMAGE, options.skipPull ?? false);
48116
48149
  logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
48117
48150
  try {
48118
- await execFileAsync$1(getDockerCmd(), [
48151
+ await execFileAsync$2(getDockerCmd(), [
48119
48152
  "network",
48120
48153
  "create",
48121
48154
  "--driver",
@@ -48151,7 +48184,7 @@ async function createTaskNetwork(options = {}) {
48151
48184
  logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
48152
48185
  let sidecarContainerId;
48153
48186
  try {
48154
- const { stdout } = await execFileAsync$1(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
48187
+ const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
48155
48188
  sidecarContainerId = stdout.trim();
48156
48189
  } catch (err) {
48157
48190
  await destroyNetworkOnly(networkName);
@@ -48199,7 +48232,7 @@ async function destroyNetworkOnly(networkName) {
48199
48232
  if (!networkName) return;
48200
48233
  const logger = getLogger().child("ecs-network");
48201
48234
  try {
48202
- await execFileAsync$1(getDockerCmd(), [
48235
+ await execFileAsync$2(getDockerCmd(), [
48203
48236
  "network",
48204
48237
  "rm",
48205
48238
  networkName
@@ -48332,7 +48365,7 @@ async function resolveSsm(entry, shape, client) {
48332
48365
 
48333
48366
  //#endregion
48334
48367
  //#region src/local/ecs-task-runner.ts
48335
- const execFileAsync = promisify(execFile);
48368
+ const execFileAsync$1 = promisify(execFile);
48336
48369
  /**
48337
48370
  * Top-level orchestrator for `cdkd local run-task`. Coordinates image
48338
48371
  * preparation, secret resolution, docker-network bring-up, container
@@ -48392,7 +48425,7 @@ async function cleanupEcsRun(state, options) {
48392
48425
  await destroyTaskNetwork(state.network);
48393
48426
  state.network = void 0;
48394
48427
  for (const v of state.dockerVolumeNames) try {
48395
- await execFileAsync(getDockerCmd(), [
48428
+ await execFileAsync$1(getDockerCmd(), [
48396
48429
  "volume",
48397
48430
  "rm",
48398
48431
  v
@@ -48458,7 +48491,7 @@ async function runEcsTask(task, options, state) {
48458
48491
  logger.info(`Starting container '${container.name}' (image=${imagePlan.get(container.name)})`);
48459
48492
  let id;
48460
48493
  try {
48461
- const { stdout } = await execFileAsync(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
48494
+ const { stdout } = await execFileAsync$1(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
48462
48495
  id = stdout.trim();
48463
48496
  } catch (err) {
48464
48497
  const e = err;
@@ -48567,7 +48600,7 @@ async function waitForContainerHealthy(containerId, displayName) {
48567
48600
  let lastStatus = "";
48568
48601
  while (Date.now() < deadline) {
48569
48602
  try {
48570
- const { stdout } = await execFileAsync(getDockerCmd(), [
48603
+ const { stdout } = await execFileAsync$1(getDockerCmd(), [
48571
48604
  "inspect",
48572
48605
  "--format",
48573
48606
  "{{.State.Health.Status}}",
@@ -48590,7 +48623,7 @@ async function waitForContainerHealthy(containerId, displayName) {
48590
48623
  }
48591
48624
  async function waitForContainerExit(containerId) {
48592
48625
  try {
48593
- const { stdout } = await execFileAsync(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
48626
+ const { stdout } = await execFileAsync$1(getDockerCmd(), ["wait", containerId], { maxBuffer: 1024 * 1024 });
48594
48627
  const code = Number.parseInt(stdout.trim(), 10);
48595
48628
  return Number.isFinite(code) ? code : 1;
48596
48629
  } catch (err) {
@@ -48600,7 +48633,7 @@ async function waitForContainerExit(containerId) {
48600
48633
  }
48601
48634
  async function stopContainer(containerId, graceSeconds) {
48602
48635
  try {
48603
- await execFileAsync(getDockerCmd(), [
48636
+ await execFileAsync$1(getDockerCmd(), [
48604
48637
  "stop",
48605
48638
  "-t",
48606
48639
  String(graceSeconds),
@@ -48725,7 +48758,7 @@ async function realizeDockerVolumes(volumes, state) {
48725
48758
  const dockerVolumeName = `cdkd-local-${v.name}-${randHex(4)}`;
48726
48759
  args.push(dockerVolumeName);
48727
48760
  try {
48728
- await execFileAsync(getDockerCmd(), args);
48761
+ await execFileAsync$1(getDockerCmd(), args);
48729
48762
  state.dockerVolumeNames.push(dockerVolumeName);
48730
48763
  logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
48731
48764
  } catch (err) {
@@ -49520,8 +49553,10 @@ const defaultWaitForExitImpl = async (containerId) => {
49520
49553
  * test-overridable function so unit tests do not need a real container.
49521
49554
  */
49522
49555
  let waitForExitImpl = defaultWaitForExitImpl;
49556
+ const defaultSleepImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
49557
+ let sleepImpl = defaultSleepImpl;
49523
49558
  function sleep(ms) {
49524
- return new Promise((resolve) => setTimeout(resolve, ms));
49559
+ return sleepImpl(ms);
49525
49560
  }
49526
49561
 
49527
49562
  //#endregion
@@ -51859,6 +51894,1069 @@ function createExportCommand() {
51859
51894
  return cmd;
51860
51895
  }
51861
51896
 
51897
+ //#endregion
51898
+ //#region src/cli/commands/migrate/cdk-cli-check.ts
51899
+ const execFileAsync = promisify(execFile);
51900
+ /**
51901
+ * Minimum aws-cdk CLI version where `cdk migrate --from-stack` is
51902
+ * considered stable (per the design doc at docs/design/465-cfn-migrate.md
51903
+ * §4 — "Version requirement"). Below this we WARN but do not block,
51904
+ * because users may have a working older release and a hard rejection
51905
+ * would be more disruptive than a warning.
51906
+ */
51907
+ const RECOMMENDED_MIN_VERSION = "2.124.0";
51908
+ /**
51909
+ * Verify that the upstream `cdk` CLI is available on PATH (or at the
51910
+ * override path passed via `--cdk-bin`) and report its version.
51911
+ *
51912
+ * Hard-errors with {@link MissingCdkCliError} when `cdk` cannot be
51913
+ * spawned (ENOENT, permission denied, etc.). Emits a `warn` field on
51914
+ * the result when the version string is below the recommended minimum
51915
+ * — the caller decides how to surface it.
51916
+ *
51917
+ * `cdk --version` output format empirically (verified 2026-05-22
51918
+ * against cdk@2.1112.0): `<MAJOR.MINOR.PATCH> (build <id>)`. The
51919
+ * version is the first whitespace-separated token; the trailing
51920
+ * `(build <id>)` is informational and ignored.
51921
+ *
51922
+ * @param cdkBinPath - Path to the `cdk` binary. Defaults to `'cdk'`
51923
+ * which uses the system PATH for resolution.
51924
+ */
51925
+ async function verifyCdkCliAvailable(cdkBinPath = "cdk") {
51926
+ let stdout;
51927
+ try {
51928
+ stdout = (await execFileAsync(cdkBinPath, ["--version"])).stdout ?? "";
51929
+ } catch (err) {
51930
+ throw new MissingCdkCliError(`Failed to run '${cdkBinPath} --version': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
51931
+ }
51932
+ const version = parseCdkVersion(stdout);
51933
+ if (!version) throw new MissingCdkCliError(`'${cdkBinPath} --version' produced unexpected output: ${JSON.stringify(stdout.trim())}`);
51934
+ if (compareSemver(version, RECOMMENDED_MIN_VERSION) < 0) return {
51935
+ version,
51936
+ 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.`
51937
+ };
51938
+ return { version };
51939
+ }
51940
+ /**
51941
+ * Parse the first semver-shaped token (`<MAJOR>.<MINOR>.<PATCH>`) out of
51942
+ * `cdk --version` stdout. Tolerates a trailing `(build <id>)` suffix.
51943
+ *
51944
+ * Returns `undefined` when no semver-shaped token is present so the
51945
+ * caller can surface the raw stdout in the error message.
51946
+ */
51947
+ function parseCdkVersion(stdout) {
51948
+ const match = stdout.match(/(\d+)\.(\d+)\.(\d+)/);
51949
+ if (!match) return void 0;
51950
+ return `${match[1]}.${match[2]}.${match[3]}`;
51951
+ }
51952
+ /**
51953
+ * Compare two semver strings (`MAJOR.MINOR.PATCH`). Returns a negative
51954
+ * number when `a < b`, zero when `a === b`, positive when `a > b`.
51955
+ *
51956
+ * Strictly numeric comparison per component — no pre-release / build
51957
+ * suffix handling needed for this use case (`cdk --version` always
51958
+ * emits a stable `X.Y.Z` triple).
51959
+ */
51960
+ function compareSemver(a, b) {
51961
+ const aParts = a.split(".").map((n) => Number.parseInt(n, 10));
51962
+ const bParts = b.split(".").map((n) => Number.parseInt(n, 10));
51963
+ for (let i = 0; i < 3; i++) {
51964
+ const aN = aParts[i] ?? 0;
51965
+ const bN = bParts[i] ?? 0;
51966
+ if (aN !== bN) return aN - bN;
51967
+ }
51968
+ return 0;
51969
+ }
51970
+
51971
+ //#endregion
51972
+ //#region src/cli/commands/migrate/cfn-stack-prefetch.ts
51973
+ /**
51974
+ * Resource types that `cdkd migrate` refuses to adopt under any
51975
+ * circumstances. The rationale per type:
51976
+ *
51977
+ * - `AWS::CloudFormation::CustomResource` — Lambda-backed Custom
51978
+ * Resources whose response protocol (cfn-response over a
51979
+ * pre-signed S3 URL) is incompatible with cdkd's import flow. The
51980
+ * backing Lambda's onCreate handler would need to be re-invoked
51981
+ * for a cdkd-side adoption to make sense, and that's structurally
51982
+ * different from the metadata-transfer migration this command
51983
+ * provides.
51984
+ *
51985
+ * - `AWS::CloudFormation::Stack` — nested stacks. cdkd has no
51986
+ * provider for this type, and the matching `cdk migrate` output
51987
+ * flattens nested stacks into separate generated apps in a way
51988
+ * that doesn't round-trip cleanly. Out of scope for #465.
51989
+ *
51990
+ * - `Custom::*` — any user-defined Custom Resource type prefix.
51991
+ * Same rationale as `AWS::CloudFormation::CustomResource`.
51992
+ */
51993
+ const HARD_REJECT_RESOURCE_TYPES = new Set(["AWS::CloudFormation::CustomResource", "AWS::CloudFormation::Stack"]);
51994
+ /**
51995
+ * Fetch the source CFn stack's current state + resources + transform
51996
+ * info. Issues 3 read-only AWS API calls (`DescribeStacks`,
51997
+ * `DescribeStackResources`, `GetTemplate(Stage=Original)`); none of
51998
+ * them mutate AWS state. The result is fed into
51999
+ * {@link validatePrefetchResult} for the hard-reject check and
52000
+ * surfaced to {@link runMigrateLibrary}'s caller (PR B) as part of
52001
+ * `RunMigrateLibraryResult`.
52002
+ *
52003
+ * `DescribeStackResources` is preferred over `ListStackResources`
52004
+ * because the response is unpaginated up to 500 resources (CFn's hard
52005
+ * stack cap) — no pagination loop needed for the migration use case.
52006
+ */
52007
+ async function prefetchCfnStack(stackName, cfnClient) {
52008
+ const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }))).Stacks?.[0];
52009
+ if (!stack) throw new LocalMigrateError(`CloudFormation stack '${stackName}' not found.`);
52010
+ const stackStatus = stack.StackStatus ?? "";
52011
+ const resourcesResp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: stackName }));
52012
+ const resources = [];
52013
+ for (const r of resourcesResp.StackResources ?? []) {
52014
+ if (!r.LogicalResourceId || !r.PhysicalResourceId || !r.ResourceType) continue;
52015
+ resources.push({
52016
+ LogicalResourceId: r.LogicalResourceId,
52017
+ PhysicalResourceId: r.PhysicalResourceId,
52018
+ ResourceType: r.ResourceType
52019
+ });
52020
+ }
52021
+ let transformInfo = {
52022
+ hasSamTransform: false,
52023
+ hasIncludeTransform: false
52024
+ };
52025
+ let sourceCfnTemplate = null;
52026
+ try {
52027
+ const tplResp = await cfnClient.send(new GetTemplateCommand({
52028
+ StackName: stackName,
52029
+ TemplateStage: "Original"
52030
+ }));
52031
+ if (tplResp.TemplateBody) {
52032
+ try {
52033
+ sourceCfnTemplate = parseCfnTemplate(tplResp.TemplateBody);
52034
+ } catch {
52035
+ sourceCfnTemplate = null;
52036
+ }
52037
+ transformInfo = detectTransformsFromParsed(sourceCfnTemplate);
52038
+ }
52039
+ } catch (err) {
52040
+ const detail = err instanceof Error ? err.message : String(err);
52041
+ getLogger().warn(`[migrate] GetTemplate failed for '${stackName}': ${detail}. Skipping transform detection.`);
52042
+ }
52043
+ return {
52044
+ stackStatus,
52045
+ resources,
52046
+ transformInfo,
52047
+ sourceCfnTemplate
52048
+ };
52049
+ }
52050
+ /**
52051
+ * Pre-flight reject when the source CFn stack contains any
52052
+ * un-migratable resource type OR is in a non-terminal state. Surfaces
52053
+ * SAM / Include transforms via the result's `transformInfo` instead —
52054
+ * the caller is responsible for emitting INFO logs.
52055
+ *
52056
+ * Per #465 Q2 (parent-session decision): every hard-reject case
52057
+ * routes the user to the standalone `cdk migrate --from-stack <name>
52058
+ * --output-dir ./tmp` flow so they can manually inspect the generated
52059
+ * code and decide how to proceed (e.g. delete the Custom Resource
52060
+ * from the source CFn stack before re-running cdkd migrate).
52061
+ */
52062
+ function validatePrefetchResult(result) {
52063
+ 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'.`);
52064
+ const offenders = [];
52065
+ for (const r of result.resources) if (isHardRejectType(r.ResourceType)) offenders.push({
52066
+ LogicalResourceId: r.LogicalResourceId,
52067
+ ResourceType: r.ResourceType
52068
+ });
52069
+ if (offenders.length === 0) return;
52070
+ 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.`);
52071
+ }
52072
+ /**
52073
+ * Whether the given resource type is in the hard-reject set. Matches
52074
+ * literal types from {@link HARD_REJECT_RESOURCE_TYPES} AND the
52075
+ * `Custom::*` prefix pattern (user-defined Custom Resource types).
52076
+ */
52077
+ function isHardRejectType(resourceType) {
52078
+ if (HARD_REJECT_RESOURCE_TYPES.has(resourceType)) return true;
52079
+ if (resourceType.startsWith("Custom::")) return true;
52080
+ return false;
52081
+ }
52082
+ /**
52083
+ * Scan an already-parsed CFn template object for stack-level
52084
+ * Transform blocks. Accepts both the scalar form (`Transform:
52085
+ * AWS::Serverless-2016-10-31`) and the array form (`Transform:
52086
+ * [AWS::Serverless-2016-10-31, ...]`).
52087
+ *
52088
+ * The caller is responsible for parsing the raw template body via the
52089
+ * CFn-aware codec; this helper operates on the parsed object so the
52090
+ * `GetTemplate` response can be parsed once and reused.
52091
+ */
52092
+ function detectTransformsFromParsed(parsed) {
52093
+ if (!parsed || typeof parsed !== "object") return {
52094
+ hasSamTransform: false,
52095
+ hasIncludeTransform: false
52096
+ };
52097
+ const transformRaw = parsed["Transform"];
52098
+ const transforms = Array.isArray(transformRaw) ? transformRaw.map((t) => String(t)) : typeof transformRaw === "string" ? [transformRaw] : [];
52099
+ return {
52100
+ hasSamTransform: transforms.some((t) => t.startsWith("AWS::Serverless")),
52101
+ hasIncludeTransform: transforms.some((t) => t === "AWS::Include" || t.startsWith("AWS::Include"))
52102
+ };
52103
+ }
52104
+
52105
+ //#endregion
52106
+ //#region src/cli/commands/migrate/output-dir-guard.ts
52107
+ /**
52108
+ * Refuse to run `cdkd migrate` when the upstream `cdk migrate` would
52109
+ * write its output into an existing non-empty directory. `cdk migrate`
52110
+ * itself silently overwrites in that case; cdkd surfaces the collision
52111
+ * up-front with the exact recovery command so the user is not left
52112
+ * with a half-overwritten CDK app.
52113
+ *
52114
+ * Behavior matrix (`outputPath = <user-supplied dir>`, `stackName =
52115
+ * <CFn stack name>`; cdk writes to `<outputPath>/<stackName>`):
52116
+ * - target dir does not exist → OK
52117
+ * - target dir exists but is empty → OK (treated as fresh)
52118
+ * - target path is a file, not a directory → typed error
52119
+ * - target dir exists and contains entries → typed error with
52120
+ * the canonical
52121
+ * `rm -rf` recovery
52122
+ * command in the
52123
+ * message
52124
+ *
52125
+ * The parent `outputPath` is NOT checked — `cdk migrate` creates
52126
+ * `<outputPath>` itself if missing, so a non-existent parent is fine.
52127
+ */
52128
+ function assertOutputDirAvailable(outputPath, stackName) {
52129
+ const targetDir = resolve(outputPath, stackName);
52130
+ if (!existsSync(targetDir)) return;
52131
+ 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}`);
52132
+ const entries = readdirSync(targetDir);
52133
+ if (entries.length === 0) return;
52134
+ 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}`);
52135
+ }
52136
+
52137
+ //#endregion
52138
+ //#region src/cli/commands/migrate/cdk-migrate-spawn.ts
52139
+ /**
52140
+ * Spawn `cdk migrate --from-stack` as a subprocess and stream its
52141
+ * output through cdkd's logger.
52142
+ *
52143
+ * Why subprocess (not Node module import): the upstream `cdk` CLI is
52144
+ * a bundled CommonJS app with no stable public API; pinning a
52145
+ * runtime-injected `aws-cdk-lib` from cdkd is impractical. Subprocess
52146
+ * isolation also gives us a clean failure boundary — non-zero exit
52147
+ * surfaces as {@link LocalMigrateError} with the captured streams
52148
+ * folded into the error message.
52149
+ *
52150
+ * Streaming is preferred over a buffered `execFile` because `cdk
52151
+ * migrate` emits progress lines (downloading the resource schema,
52152
+ * synthesizing the L1 code for each resource) that users expect to
52153
+ * see in real time. Captured streams are still returned for caller
52154
+ * inspection.
52155
+ */
52156
+ async function spawnCdkMigrate(opts) {
52157
+ const logger = getLogger();
52158
+ const cdkBin = opts.cdkBinPath ?? "cdk";
52159
+ const language = opts.language ?? "typescript";
52160
+ const outputDir = resolve(opts.outputPath, opts.stackName);
52161
+ const args = [
52162
+ "migrate",
52163
+ "--from-stack",
52164
+ "--stack-name",
52165
+ opts.fromStackName,
52166
+ "--output-path",
52167
+ opts.outputPath,
52168
+ "--language",
52169
+ language
52170
+ ];
52171
+ if (opts.region) args.push("--region", opts.region);
52172
+ if (opts.account) args.push("--account", opts.account);
52173
+ if (opts.profile) args.push("--profile", opts.profile);
52174
+ for (const filter of opts.filters ?? []) args.push("--filter", filter);
52175
+ const env = {
52176
+ ...process.env,
52177
+ ...opts.extraEnv ?? {}
52178
+ };
52179
+ logger.info(`[cdk-migrate] ${cdkBin} ${args.join(" ")}`);
52180
+ return await new Promise((resolvePromise, rejectPromise) => {
52181
+ let stdout = "";
52182
+ let stderr = "";
52183
+ let child;
52184
+ try {
52185
+ child = spawn(cdkBin, args, {
52186
+ env,
52187
+ stdio: [
52188
+ "ignore",
52189
+ "pipe",
52190
+ "pipe"
52191
+ ]
52192
+ });
52193
+ } catch (err) {
52194
+ rejectPromise(new LocalMigrateError(`Failed to spawn '${cdkBin} migrate': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
52195
+ return;
52196
+ }
52197
+ child.stdout?.on("data", (chunk) => {
52198
+ const text = chunk.toString("utf-8");
52199
+ stdout += text;
52200
+ const trimmed = text.replace(/\n$/, "");
52201
+ if (trimmed) logger.info(`[cdk-migrate] ${trimmed}`);
52202
+ });
52203
+ child.stderr?.on("data", (chunk) => {
52204
+ const text = chunk.toString("utf-8");
52205
+ stderr += text;
52206
+ const trimmed = text.replace(/\n$/, "");
52207
+ if (trimmed) logger.warn(`[cdk-migrate] ${trimmed}`);
52208
+ });
52209
+ child.on("error", (err) => {
52210
+ rejectPromise(new LocalMigrateError(`'${cdkBin} migrate' subprocess error: ${err.message}`, err));
52211
+ });
52212
+ child.on("close", (code, signal) => {
52213
+ if (code === 0) {
52214
+ resolvePromise({
52215
+ outputDir,
52216
+ stdout,
52217
+ stderr
52218
+ });
52219
+ return;
52220
+ }
52221
+ 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`));
52222
+ });
52223
+ });
52224
+ }
52225
+
52226
+ //#endregion
52227
+ //#region src/cli/commands/migrate/synth-after-migrate.ts
52228
+ /**
52229
+ * Run `npm install` inside the generated CDK app directory so the
52230
+ * `cdk synth` step (which dynamically loads `aws-cdk-lib`) has its
52231
+ * dependencies in place.
52232
+ *
52233
+ * Skippable via `skipInstall: true` — CI users with a pre-populated
52234
+ * node_modules cache can save the ~30s `npm install` round-trip.
52235
+ *
52236
+ * Streams stdout / stderr through cdkd's logger so users see progress
52237
+ * for the often-slow npm step. Non-zero exit surfaces as a typed
52238
+ * {@link LocalMigrateError} with the captured streams folded into
52239
+ * the error message.
52240
+ */
52241
+ async function installGeneratedAppDeps(outputDir, opts) {
52242
+ const logger = getLogger();
52243
+ if (opts.skipInstall) {
52244
+ logger.info(`[cdk-migrate] Skipping 'npm install' (--skip-install).`);
52245
+ return;
52246
+ }
52247
+ const absDir = resolve(outputDir);
52248
+ if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'npm install'.`);
52249
+ logger.info(`[cdk-migrate] Running 'npm install' in ${absDir}...`);
52250
+ await runStreamingCommand("npm", ["install"], absDir, opts.extraEnv ? {
52251
+ ...process.env,
52252
+ ...opts.extraEnv
52253
+ } : void 0, "npm install");
52254
+ }
52255
+ /**
52256
+ * Run `cdk synth --quiet` inside the generated CDK app directory and
52257
+ * return both the assembly dir path and the parsed root-stack
52258
+ * template.
52259
+ *
52260
+ * `--quiet` suppresses the noisy template echo that `cdk synth`
52261
+ * defaults to on stdout — we read the template from disk
52262
+ * (`cdk.out/<StackName>.template.json`) instead.
52263
+ *
52264
+ * Skippable via `skipSynth: true` — useful when PR B's orchestrator
52265
+ * needs the codegen output without running synth (e.g. preview /
52266
+ * dry-run modes).
52267
+ */
52268
+ async function synthGeneratedApp(outputDir, opts) {
52269
+ const logger = getLogger();
52270
+ const absDir = resolve(outputDir);
52271
+ const assemblyDir = join(absDir, "cdk.out");
52272
+ if (opts.skipSynth) {
52273
+ logger.info(`[cdk-migrate] Skipping 'cdk synth' (--skip-synth).`);
52274
+ return {
52275
+ assemblyDir,
52276
+ templateBody: null
52277
+ };
52278
+ }
52279
+ if (!existsSync(absDir)) throw new LocalMigrateError(`Generated app directory '${absDir}' does not exist; cannot run 'cdk synth'.`);
52280
+ const cdkBin = opts.cdkBinPath ?? "cdk";
52281
+ const env = {
52282
+ ...process.env,
52283
+ ...opts.extraEnv ?? {}
52284
+ };
52285
+ logger.info(`[cdk-migrate] Running '${cdkBin} synth --quiet' in ${absDir}...`);
52286
+ await runStreamingCommand(cdkBin, ["synth", "--quiet"], absDir, env, `${cdkBin} synth`);
52287
+ if (!existsSync(assemblyDir)) throw new LocalMigrateError(`'cdk synth' completed but produced no '${assemblyDir}' directory.`);
52288
+ const templates = readdirSync(assemblyDir).filter((f) => f.endsWith(".template.json"));
52289
+ if (templates.length === 0) return {
52290
+ assemblyDir,
52291
+ templateBody: null
52292
+ };
52293
+ const templatePath = join(assemblyDir, templates[0]);
52294
+ let templateBody;
52295
+ try {
52296
+ const raw = readFileSync(templatePath, "utf-8");
52297
+ templateBody = JSON.parse(raw);
52298
+ } catch (err) {
52299
+ throw new LocalMigrateError(`Failed to parse generated template '${templatePath}': ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0);
52300
+ }
52301
+ return {
52302
+ assemblyDir,
52303
+ templateBody
52304
+ };
52305
+ }
52306
+ /**
52307
+ * Helper — spawn a subprocess in the given working directory, stream
52308
+ * its output through cdkd's logger, and reject with a typed
52309
+ * {@link LocalMigrateError} on non-zero exit.
52310
+ *
52311
+ * Factored out so the `npm install` and `cdk synth` call sites use the
52312
+ * same shape; both expose progress to the user and surface failures
52313
+ * with the captured streams.
52314
+ */
52315
+ async function runStreamingCommand(bin, args, cwd, env, label) {
52316
+ const logger = getLogger();
52317
+ return await new Promise((resolvePromise, rejectPromise) => {
52318
+ let stdout = "";
52319
+ let stderr = "";
52320
+ let child;
52321
+ try {
52322
+ child = spawn(bin, args, {
52323
+ cwd,
52324
+ env: env ?? process.env,
52325
+ stdio: [
52326
+ "ignore",
52327
+ "pipe",
52328
+ "pipe"
52329
+ ]
52330
+ });
52331
+ } catch (err) {
52332
+ rejectPromise(new LocalMigrateError(`Failed to spawn ${label}: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0));
52333
+ return;
52334
+ }
52335
+ child.stdout?.on("data", (chunk) => {
52336
+ const text = chunk.toString("utf-8");
52337
+ stdout += text;
52338
+ const trimmed = text.replace(/\n$/, "");
52339
+ if (trimmed) logger.info(`[${label}] ${trimmed}`);
52340
+ });
52341
+ child.stderr?.on("data", (chunk) => {
52342
+ const text = chunk.toString("utf-8");
52343
+ stderr += text;
52344
+ const trimmed = text.replace(/\n$/, "");
52345
+ if (trimmed) logger.warn(`[${label}] ${trimmed}`);
52346
+ });
52347
+ child.on("error", (err) => {
52348
+ rejectPromise(new LocalMigrateError(`${label} subprocess error: ${err.message}`, err));
52349
+ });
52350
+ child.on("close", (code, signal) => {
52351
+ if (code === 0) {
52352
+ resolvePromise();
52353
+ return;
52354
+ }
52355
+ 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`));
52356
+ });
52357
+ });
52358
+ }
52359
+
52360
+ //#endregion
52361
+ //#region src/cli/commands/migrate/index.ts
52362
+ /**
52363
+ * Build an extra-env bag for subprocesses (`npm install`, `cdk synth`)
52364
+ * so they inherit the AWS profile the caller selected via `--profile`.
52365
+ *
52366
+ * Without this, `cdk synth` falls back to the SDK default credential
52367
+ * chain (or no credentials at all) when the user runs cdkd under a
52368
+ * non-default profile — which silently produces a template synthesized
52369
+ * against the wrong account / region context.
52370
+ */
52371
+ function buildAwsEnv(opts) {
52372
+ return opts.profile ? { AWS_PROFILE: opts.profile } : {};
52373
+ }
52374
+ /**
52375
+ * PR A library entry point — orchestrates the codegen + synth half of
52376
+ * `cdkd migrate --from-cfn-stack` without writing cdkd state, running
52377
+ * import, or touching the source CFn stack.
52378
+ *
52379
+ * Flow:
52380
+ * 1. `verifyCdkCliAvailable` — hard-fail if `cdk` is missing.
52381
+ * 2. `prefetchCfnStack` — read source stack state + resources +
52382
+ * transform info.
52383
+ * 3. `validatePrefetchResult` — reject CR / nested-stack / non-
52384
+ * terminal state; INFO log SAM / Include transforms.
52385
+ * 4. `assertOutputDirAvailable` — refuse pre-existing non-empty
52386
+ * output dir.
52387
+ * 5. `spawnCdkMigrate` — run `cdk migrate --from-stack` subprocess.
52388
+ * 6. `installGeneratedAppDeps` (gated by `skipInstall`).
52389
+ * 7. `synthGeneratedApp` (gated by `skipSynth`).
52390
+ * 8. Return every artifact PR B will consume.
52391
+ *
52392
+ * Per #465 Q4 (parent-session decision): PR A is library-only. The CLI
52393
+ * command (`cdkd migrate`), state writes, retire flow, and resource-
52394
+ * mapping algorithm all live in PR B.
52395
+ */
52396
+ async function runMigrateLibrary(opts) {
52397
+ const logger = getLogger();
52398
+ const stackName = opts.fromCfnStack;
52399
+ const outputPath = opts.outputDir ?? resolve(process.cwd(), stackName);
52400
+ logger.info(`[migrate] Verifying upstream 'cdk' CLI...`);
52401
+ const cliCheck = await verifyCdkCliAvailable(opts.cdkBinPath);
52402
+ logger.info(`[migrate] Using cdk CLI v${cliCheck.version}`);
52403
+ if (cliCheck.warn) logger.warn(`[migrate] ${cliCheck.warn}`);
52404
+ logger.info(`[migrate] Pre-fetching CloudFormation stack '${stackName}'...`);
52405
+ const cfnClient = buildCfnClient(opts);
52406
+ let prefetch;
52407
+ try {
52408
+ prefetch = await prefetchCfnStack(stackName, cfnClient);
52409
+ } finally {
52410
+ cfnClient.destroy?.();
52411
+ }
52412
+ validatePrefetchResult(prefetch);
52413
+ 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).");
52414
+ 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.");
52415
+ assertOutputDirAvailable(outputPath, stackName);
52416
+ const spawnResult = await spawnCdkMigrate({
52417
+ stackName,
52418
+ fromStackName: stackName,
52419
+ outputPath,
52420
+ ...opts.language && { language: opts.language },
52421
+ ...opts.region && { region: opts.region },
52422
+ ...opts.account && { account: opts.account },
52423
+ ...opts.filters && { filters: opts.filters },
52424
+ ...opts.profile && { profile: opts.profile },
52425
+ ...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath }
52426
+ });
52427
+ await installGeneratedAppDeps(spawnResult.outputDir, {
52428
+ skipInstall: opts.skipInstall ?? false,
52429
+ extraEnv: buildAwsEnv(opts)
52430
+ });
52431
+ const synthResult = await synthGeneratedApp(spawnResult.outputDir, {
52432
+ skipSynth: opts.skipSynth ?? false,
52433
+ ...opts.cdkBinPath && { cdkBinPath: opts.cdkBinPath },
52434
+ extraEnv: buildAwsEnv(opts)
52435
+ });
52436
+ 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.`);
52437
+ return {
52438
+ outputDir: spawnResult.outputDir,
52439
+ assemblyDir: synthResult.assemblyDir,
52440
+ templateBody: synthResult.templateBody,
52441
+ sourceCfnTemplate: prefetch.sourceCfnTemplate,
52442
+ sourceResources: prefetch.resources
52443
+ };
52444
+ }
52445
+ /**
52446
+ * Build a CloudFormation client using cdkd's shared {@link AwsClients}
52447
+ * factory so the credential chain matches every other cdkd command.
52448
+ *
52449
+ * Keeping the construction in one place makes the per-command CFn-
52450
+ * client recipe a single readable hook for future plumbing (e.g.
52451
+ * threading PR B's `--role-arn` STS-assume).
52452
+ */
52453
+ function buildCfnClient(opts) {
52454
+ const config = {};
52455
+ if (opts.region) config.region = opts.region;
52456
+ if (opts.profile) config.profile = opts.profile;
52457
+ return new AwsClients(config).getCloudFormationClient();
52458
+ }
52459
+
52460
+ //#endregion
52461
+ //#region src/cli/commands/migrate/resource-mapper.ts
52462
+ /**
52463
+ * Run the 2-pass mapping algorithm against `(sourceCfnTemplate,
52464
+ * synthTemplate, sourceResources)` and return the resolved mapping plus
52465
+ * any unmatched entries.
52466
+ *
52467
+ * Pure-functional: no AWS calls, no I/O, no mutation of inputs. Safe to
52468
+ * call repeatedly in tests against shared fixtures.
52469
+ *
52470
+ * Throws when an override references a synth id that does not exist —
52471
+ * this is a user error in the hand-edited mapping JSON and surfacing
52472
+ * the available synth ids in the message is the path back to a working
52473
+ * config.
52474
+ */
52475
+ function buildResourceMapping(opts) {
52476
+ const sourceEntries = extractSourceResources(opts.sourceCfnTemplate);
52477
+ const synthEntries = extractSynthResources(opts.synthTemplate);
52478
+ const physicalIdByLogicalId = /* @__PURE__ */ new Map();
52479
+ for (const r of opts.sourceResources) physicalIdByLogicalId.set(r.LogicalResourceId, r.PhysicalResourceId);
52480
+ const overrides = opts.overrides ?? {};
52481
+ const synthLogicalIds = new Set(synthEntries.map((s) => s.logicalId));
52482
+ 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(", ")}`);
52483
+ const missingPhysicalIds = [];
52484
+ for (const src of sourceEntries) if (!physicalIdByLogicalId.has(src.logicalId)) missingPhysicalIds.push(` - ${src.logicalId} (${src.type})`);
52485
+ 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.`);
52486
+ const synthByLastPathSegment = indexSynthByLastPathSegment(synthEntries);
52487
+ const synthByLogicalId = new Map(synthEntries.map((s) => [s.logicalId, s]));
52488
+ const claimedSynthIds = /* @__PURE__ */ new Set();
52489
+ const pairs = [];
52490
+ const unmatched = [];
52491
+ const sourcesHandledByOverride = /* @__PURE__ */ new Set();
52492
+ for (const [srcId, synthId] of Object.entries(overrides)) {
52493
+ const src = sourceEntries.find((e) => e.logicalId === srcId);
52494
+ if (!src) {
52495
+ const availableSrc = sourceEntries.map((e) => e.logicalId).sort();
52496
+ 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(", ")}`);
52497
+ }
52498
+ const synth = synthByLogicalId.get(synthId);
52499
+ if (!synth) continue;
52500
+ const physicalId = physicalIdByLogicalId.get(src.logicalId);
52501
+ pairs.push({
52502
+ sourceLogicalId: src.logicalId,
52503
+ synthLogicalId: synth.logicalId,
52504
+ physicalId,
52505
+ resourceType: src.type
52506
+ });
52507
+ claimedSynthIds.add(synth.logicalId);
52508
+ sourcesHandledByOverride.add(src.logicalId);
52509
+ }
52510
+ const passOnePending = [];
52511
+ for (const src of sourceEntries) {
52512
+ if (sourcesHandledByOverride.has(src.logicalId)) continue;
52513
+ const candidatesForLastSegment = synthByLastPathSegment.get(src.logicalId) ?? [];
52514
+ const rawCandidateCount = candidatesForLastSegment.length;
52515
+ const availableCandidates = candidatesForLastSegment.filter((c) => !claimedSynthIds.has(c.logicalId));
52516
+ if (rawCandidateCount === 1 && availableCandidates.length === 1) {
52517
+ const synth = availableCandidates[0];
52518
+ pairs.push({
52519
+ sourceLogicalId: src.logicalId,
52520
+ synthLogicalId: synth.logicalId,
52521
+ physicalId: physicalIdByLogicalId.get(src.logicalId),
52522
+ resourceType: src.type
52523
+ });
52524
+ claimedSynthIds.add(synth.logicalId);
52525
+ } else passOnePending.push(src);
52526
+ }
52527
+ for (const src of passOnePending) {
52528
+ const candidatesSameType = synthEntries.filter((s) => s.type === src.type && !claimedSynthIds.has(s.logicalId));
52529
+ const propertyMatches = candidatesSameType.filter((s) => deepEqualIgnoreNoValue(src.properties, s.properties));
52530
+ if (propertyMatches.length === 1) {
52531
+ const synth = propertyMatches[0];
52532
+ pairs.push({
52533
+ sourceLogicalId: src.logicalId,
52534
+ synthLogicalId: synth.logicalId,
52535
+ physicalId: physicalIdByLogicalId.get(src.logicalId),
52536
+ resourceType: src.type
52537
+ });
52538
+ claimedSynthIds.add(synth.logicalId);
52539
+ } else {
52540
+ const isCollision = (synthByLastPathSegment.get(src.logicalId) ?? []).length >= 2;
52541
+ unmatched.push({
52542
+ sourceLogicalId: src.logicalId,
52543
+ resourceType: src.type,
52544
+ candidates: candidatesSameType.map((c) => c.logicalId).sort(),
52545
+ reason: isCollision ? "logical-id-collision" : "no-match"
52546
+ });
52547
+ }
52548
+ }
52549
+ const mapping = {};
52550
+ for (const p of pairs) mapping[p.sourceLogicalId] = p.synthLogicalId;
52551
+ return {
52552
+ mapping,
52553
+ pairs,
52554
+ unmatched
52555
+ };
52556
+ }
52557
+ /**
52558
+ * Walk a parsed CFn template's `Resources` and return one entry per
52559
+ * resource. `Type` defaults to `''` and `Properties` to `{}` when the
52560
+ * template omits them (CFn allows resources with only `DeletionPolicy`
52561
+ * + `Type`); the mapper's downstream deep-equal handles `{}` cleanly.
52562
+ *
52563
+ * Top-level keys other than `Resources` (Conditions / Parameters /
52564
+ * Rules / Outputs / Mappings / Metadata) are NOT walked — the mapping
52565
+ * is resource-to-resource only, and §5.5 of the design doc spells out
52566
+ * the four CDK-synth injections that live in those sibling keys.
52567
+ */
52568
+ function extractSourceResources(template) {
52569
+ if (!template || typeof template !== "object") return [];
52570
+ const resources = template["Resources"];
52571
+ if (!resources || typeof resources !== "object") return [];
52572
+ const out = [];
52573
+ for (const [logicalId, raw] of Object.entries(resources)) {
52574
+ if (!raw || typeof raw !== "object") continue;
52575
+ const r = raw;
52576
+ const type = typeof r["Type"] === "string" ? r["Type"] : "";
52577
+ const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
52578
+ out.push({
52579
+ logicalId,
52580
+ type,
52581
+ properties: props
52582
+ });
52583
+ }
52584
+ return out;
52585
+ }
52586
+ /**
52587
+ * Walk the synth template's `Resources` and return one entry per
52588
+ * resource, EXCLUDING `AWS::CDK::Metadata` (synth-only sentinel that has
52589
+ * no source counterpart). Captures `Metadata['aws:cdk:path']` so Pass 1
52590
+ * can match against the last `/`-separated segment.
52591
+ *
52592
+ * The four other CDK-synth injections (CDKMetadataAvailable Condition,
52593
+ * BootstrapVersion Parameter, CheckBootstrapVersion Rule) live outside
52594
+ * `Resources` and are structurally excluded by this function.
52595
+ */
52596
+ function extractSynthResources(template) {
52597
+ if (!template || typeof template !== "object") return [];
52598
+ const resources = template["Resources"];
52599
+ if (!resources || typeof resources !== "object") return [];
52600
+ const out = [];
52601
+ for (const [logicalId, raw] of Object.entries(resources)) {
52602
+ if (!raw || typeof raw !== "object") continue;
52603
+ const r = raw;
52604
+ const type = typeof r["Type"] === "string" ? r["Type"] : "";
52605
+ if (type === "AWS::CDK::Metadata") continue;
52606
+ const props = r["Properties"] && typeof r["Properties"] === "object" ? r["Properties"] : {};
52607
+ const meta = r["Metadata"] && typeof r["Metadata"] === "object" ? r["Metadata"] : {};
52608
+ const path = typeof meta["aws:cdk:path"] === "string" ? meta["aws:cdk:path"] : "";
52609
+ out.push({
52610
+ logicalId,
52611
+ type,
52612
+ properties: props,
52613
+ awsCdkPath: path
52614
+ });
52615
+ }
52616
+ return out;
52617
+ }
52618
+ /**
52619
+ * Group synth entries by the LAST `/`-separated segment of their
52620
+ * `aws:cdk:path` metadata so Pass 1 can look up the source logical id
52621
+ * directly. Per [docs/design/465-cfn-migrate.md](../../../../docs/design/465-cfn-migrate.md) §5.5:
52622
+ * `cdk migrate` emits `<StackName>/<LogicalId>` as the metadata value,
52623
+ * so the last segment IS the source logical id in the typical case.
52624
+ *
52625
+ * Entries without an `aws:cdk:path` are silently skipped — they cannot
52626
+ * be addressed by source logical id and will only be reachable via
52627
+ * Pass 2's Properties match.
52628
+ */
52629
+ function indexSynthByLastPathSegment(entries) {
52630
+ const out = /* @__PURE__ */ new Map();
52631
+ for (const e of entries) {
52632
+ if (!e.awsCdkPath) continue;
52633
+ const lastSegment = e.awsCdkPath.split("/").pop() ?? "";
52634
+ if (!lastSegment) continue;
52635
+ const bucket = out.get(lastSegment);
52636
+ if (bucket) bucket.push(e);
52637
+ else out.set(lastSegment, [e]);
52638
+ }
52639
+ return out;
52640
+ }
52641
+ /**
52642
+ * Recursive deep equality between two values, with two CFn-specific
52643
+ * accommodations:
52644
+ *
52645
+ * 1. Object key order is NOT significant — `JSON.stringify` would
52646
+ * diverge on the canonical-source-vs-synth-alphabetized case
52647
+ * (verified by PR A's empirical run, §5.5 point 3) so we walk the
52648
+ * keys explicitly.
52649
+ * 2. `AWS::NoValue` placeholder values are stripped before compare —
52650
+ * `cdk migrate`'s codegen sometimes emits the placeholder for a
52651
+ * property the source template simply omitted, and the structural
52652
+ * intent matches. Source side never has the placeholder; synth side
52653
+ * can on either side of a nested object.
52654
+ *
52655
+ * Arrays are compared positionally (order IS significant — Tags arrays
52656
+ * and similar carry ordering semantics).
52657
+ */
52658
+ function deepEqualIgnoreNoValue(a, b) {
52659
+ if (a === b) return true;
52660
+ if (isAwsNoValue(a) !== isAwsNoValue(b)) return false;
52661
+ if (isAwsNoValue(a) && isAwsNoValue(b)) return true;
52662
+ if (typeof a !== typeof b) return false;
52663
+ if (a === null || b === null) return a === b;
52664
+ if (typeof a !== "object") return a === b;
52665
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
52666
+ if (Array.isArray(a) && Array.isArray(b)) {
52667
+ if (a.length !== b.length) return false;
52668
+ for (let i = 0; i < a.length; i++) if (!deepEqualIgnoreNoValue(a[i], b[i])) return false;
52669
+ return true;
52670
+ }
52671
+ const aObj = a;
52672
+ const bObj = b;
52673
+ const aKeys = Object.keys(aObj).filter((k) => !isAwsNoValue(aObj[k]) && aObj[k] !== void 0);
52674
+ const bKeys = Object.keys(bObj).filter((k) => !isAwsNoValue(bObj[k]) && bObj[k] !== void 0);
52675
+ if (aKeys.length !== bKeys.length) return false;
52676
+ const aKeySet = new Set(aKeys);
52677
+ for (const k of bKeys) {
52678
+ if (!aKeySet.has(k)) return false;
52679
+ if (!deepEqualIgnoreNoValue(aObj[k], bObj[k])) return false;
52680
+ }
52681
+ return true;
52682
+ }
52683
+ /**
52684
+ * True when the value is the CFn `AWS::NoValue` placeholder
52685
+ * (`{Ref: 'AWS::NoValue'}`). `cdk migrate`'s codegen emits this for
52686
+ * conditional properties the source template omitted; the structural
52687
+ * intent matches and deep-equal must treat them as absent.
52688
+ */
52689
+ function isAwsNoValue(v) {
52690
+ if (!v || typeof v !== "object") return false;
52691
+ const obj = v;
52692
+ const keys = Object.keys(obj);
52693
+ return keys.length === 1 && keys[0] === "Ref" && obj["Ref"] === "AWS::NoValue";
52694
+ }
52695
+
52696
+ //#endregion
52697
+ //#region src/cli/commands/migrate/resource-mapping-file.ts
52698
+ /** Conventional filename inside the migrate output dir. */
52699
+ const RESOURCE_MAPPING_FILENAME = "cdkd-resource-mapping.json";
52700
+ /**
52701
+ * Build the on-disk file shape from a {@link ResourceMappingResult} +
52702
+ * the source / output stack names, then write it to
52703
+ * `<outputDir>/cdkd-resource-mapping.json` (overwriting any prior file
52704
+ * on the same path — same idempotency contract as `cdk migrate`'s own
52705
+ * codegen, and the user expects a re-run to refresh stale data).
52706
+ *
52707
+ * Returns the absolute on-disk path so the orchestrator can name it in
52708
+ * the error message when the mapping is incomplete.
52709
+ */
52710
+ function writeMappingFile(outputDir, args) {
52711
+ const path = join(outputDir, RESOURCE_MAPPING_FILENAME);
52712
+ const file = {
52713
+ version: 1,
52714
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
52715
+ sourceStack: args.sourceStack,
52716
+ outputStack: args.outputStack,
52717
+ mapping: args.result.mapping
52718
+ };
52719
+ if (args.result.unmatched.length > 0) file._unmatched = args.result.unmatched;
52720
+ writeFileSync(path, JSON.stringify(file, null, 2) + "\n", "utf-8");
52721
+ return path;
52722
+ }
52723
+ /**
52724
+ * Read a user-supplied resource-mapping file from disk and return the
52725
+ * `{<srcLogicalId>: <synthLogicalId>}` overrides for the mapper.
52726
+ *
52727
+ * Tolerates `_unmatched` on input (a user that re-runs against a
52728
+ * partial-failure file without editing should hit the same failure on
52729
+ * the resolution side — not a parse error here). Hard-errors on
52730
+ * structurally invalid input (missing `mapping`, non-object root,
52731
+ * non-string values, wrong `version`).
52732
+ *
52733
+ * Surfaces every shape error as `LocalMigrateError` (exit code 2) so
52734
+ * the CLI handler routes it through the normal error path.
52735
+ */
52736
+ function readMappingFile(path) {
52737
+ if (!existsSync(path)) throw new LocalMigrateError(`Resource-mapping file not found: ${path}. Drop the --resource-mapping flag or supply a valid path.`);
52738
+ let raw;
52739
+ try {
52740
+ raw = JSON.parse(readFileSync(path, "utf-8"));
52741
+ } catch (err) {
52742
+ throw new LocalMigrateError(`Resource-mapping file '${path}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
52743
+ }
52744
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new LocalMigrateError(`Resource-mapping file '${path}' must contain a JSON object at the top level.`);
52745
+ const obj = raw;
52746
+ if (obj["version"] !== 1) throw new LocalMigrateError(`Resource-mapping file '${path}' has unsupported version '${String(obj["version"])}'. Expected version 1.`);
52747
+ if (typeof obj["sourceStack"] !== "string" || obj["sourceStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'sourceStack' field.`);
52748
+ if (typeof obj["outputStack"] !== "string" || obj["outputStack"].length === 0) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'outputStack' field.`);
52749
+ if (typeof obj["generatedAt"] !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'generatedAt' field.`);
52750
+ const mappingRaw = obj["mapping"];
52751
+ if (!mappingRaw || typeof mappingRaw !== "object" || Array.isArray(mappingRaw)) throw new LocalMigrateError(`Resource-mapping file '${path}' is missing the required 'mapping' object.`);
52752
+ const mapping = {};
52753
+ for (const [k, v] of Object.entries(mappingRaw)) {
52754
+ if (typeof v !== "string") throw new LocalMigrateError(`Resource-mapping file '${path}' has a non-string value for key '${k}'.`);
52755
+ mapping[k] = v;
52756
+ }
52757
+ const result = {
52758
+ version: 1,
52759
+ generatedAt: obj["generatedAt"],
52760
+ sourceStack: obj["sourceStack"],
52761
+ outputStack: obj["outputStack"],
52762
+ mapping
52763
+ };
52764
+ if (Array.isArray(obj["_unmatched"])) {
52765
+ const cleaned = [];
52766
+ for (const entry of obj["_unmatched"]) {
52767
+ if (!entry || typeof entry !== "object") continue;
52768
+ const e = entry;
52769
+ if (typeof e["sourceLogicalId"] !== "string" || typeof e["resourceType"] !== "string" || !Array.isArray(e["candidates"]) || e["reason"] !== "no-match" && e["reason"] !== "logical-id-collision") continue;
52770
+ cleaned.push({
52771
+ sourceLogicalId: e["sourceLogicalId"],
52772
+ resourceType: e["resourceType"],
52773
+ candidates: e["candidates"].filter((c) => typeof c === "string"),
52774
+ reason: e["reason"]
52775
+ });
52776
+ }
52777
+ if (cleaned.length > 0) result._unmatched = cleaned;
52778
+ }
52779
+ return result;
52780
+ }
52781
+
52782
+ //#endregion
52783
+ //#region src/cli/commands/migrate-command.ts
52784
+ /**
52785
+ * `cdkd migrate --from-cfn-stack <name>` end-to-end orchestrator.
52786
+ *
52787
+ * Closes the CLI half of [#465](https://github.com/go-to-k/cdkd/issues/465).
52788
+ * Reads the source CFn stack, runs upstream `cdk migrate` via the PR A
52789
+ * library, builds the source → synth logical-ID mapping with the 2-pass
52790
+ * algorithm, writes a `cdkd-resource-mapping.json` audit file, prompts
52791
+ * for confirmation, invokes `cdkd import` to write state under the
52792
+ * synth logical IDs, and (when `--retire-cfn-stack` is set) finally
52793
+ * retires the source CFn stack so management responsibility transfers
52794
+ * fully to cdkd.
52795
+ *
52796
+ * AWS resources are never modified — the migration is a metadata
52797
+ * transfer only. See [docs/design/465-cfn-migrate.md](../../../docs/design/465-cfn-migrate.md) §7
52798
+ * for the post-migration state matrix.
52799
+ */
52800
+ async function migrateCommandAction(positionalStack, options) {
52801
+ const logger = getLogger();
52802
+ if (options.verbose) {
52803
+ logger.setLevel("debug");
52804
+ process.env["CDKD_NO_LIVE"] = "1";
52805
+ }
52806
+ const sourceCfnStackName = options.fromCfnStack ?? positionalStack;
52807
+ if (!sourceCfnStackName) throw new LocalMigrateError("Missing required argument: --from-cfn-stack <name> (or pass the stack name positionally).");
52808
+ 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.");
52809
+ 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).");
52810
+ 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.");
52811
+ await applyRoleArnIfSet({
52812
+ roleArn: options.roleArn,
52813
+ region: options.region
52814
+ });
52815
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
52816
+ const outputDir = resolve(options.outputDir ?? sourceCfnStackName);
52817
+ logger.info(`[migrate] Source CFn stack: ${sourceCfnStackName}`);
52818
+ logger.info(`[migrate] Output directory: ${outputDir}`);
52819
+ const libResult = await runMigrateLibrary({
52820
+ fromCfnStack: sourceCfnStackName,
52821
+ outputDir,
52822
+ language: options.language ?? "typescript",
52823
+ ...options.region && { region: options.region },
52824
+ ...options.account && { account: options.account },
52825
+ ...options.filter && options.filter.length > 0 && { filters: options.filter },
52826
+ ...options.profile && { profile: options.profile },
52827
+ ...options.cdkBin && { cdkBinPath: options.cdkBin },
52828
+ skipInstall: options.skipInstall ?? false,
52829
+ skipSynth: options.skipSynth ?? false
52830
+ });
52831
+ if (options.skipSynth || !libResult.templateBody) {
52832
+ logger.info(`[migrate] --skip-synth: generated CDK app at ${libResult.outputDir}. Re-run without --skip-synth to write cdkd state.`);
52833
+ return;
52834
+ }
52835
+ let overrides;
52836
+ if (options.resourceMapping) {
52837
+ const mappingPath = resolve(options.resourceMapping);
52838
+ logger.info(`[migrate] Loading user-supplied resource mapping from ${mappingPath}`);
52839
+ overrides = readMappingFile(mappingPath).mapping;
52840
+ }
52841
+ let mappingResult;
52842
+ try {
52843
+ mappingResult = buildResourceMapping({
52844
+ sourceCfnTemplate: libResult.sourceCfnTemplate,
52845
+ synthTemplate: libResult.templateBody,
52846
+ sourceResources: libResult.sourceResources,
52847
+ ...overrides && { overrides }
52848
+ });
52849
+ } catch (err) {
52850
+ throw new LocalMigrateError(err instanceof Error ? err.message : String(err));
52851
+ }
52852
+ const outputStack = sourceCfnStackName;
52853
+ const mappingFilePath = writeMappingFile(libResult.outputDir, {
52854
+ sourceStack: sourceCfnStackName,
52855
+ outputStack,
52856
+ result: mappingResult
52857
+ });
52858
+ logger.info(`[migrate] Resource mapping written to ${mappingFilePath}`);
52859
+ if (mappingResult.unmatched.length > 0) {
52860
+ const lines = mappingResult.unmatched.map((u) => {
52861
+ const candidatesStr = u.candidates.length > 0 ? ` (synth candidates of same Type: ${u.candidates.join(", ")})` : " (no synth resource has the same Type)";
52862
+ return ` - ${u.sourceLogicalId} (${u.resourceType}) [${u.reason}]${candidatesStr}`;
52863
+ });
52864
+ 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.)`);
52865
+ }
52866
+ printMappingTable(mappingResult, sourceCfnStackName, outputStack);
52867
+ if (options.dryRun) {
52868
+ logger.info(`[migrate] --dry-run: would import ${mappingResult.pairs.length} resource(s) into cdkd state under stack '${outputStack}' (region '${region}'). State NOT written.`);
52869
+ return;
52870
+ }
52871
+ if (!options.yes) {
52872
+ if (!await promptConfirm(`Import ${mappingResult.pairs.length} resource(s) into cdkd state for stack '${outputStack}' (${region})?`)) {
52873
+ logger.info("[migrate] Cancelled by user. No state written.");
52874
+ return;
52875
+ }
52876
+ }
52877
+ const importMapping = {};
52878
+ for (const p of mappingResult.pairs) importMapping[p.synthLogicalId] = p.physicalId;
52879
+ const resolvedStateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
52880
+ await runImport(outputStack, {
52881
+ app: libResult.assemblyDir,
52882
+ statePrefix: options.statePrefix ?? "cdkd",
52883
+ resourceMappingInline: JSON.stringify(importMapping),
52884
+ auto: false,
52885
+ dryRun: false,
52886
+ yes: true,
52887
+ force: false,
52888
+ verbose: options.verbose ?? false,
52889
+ stateBucket: resolvedStateBucket,
52890
+ ...options.region && { region: options.region },
52891
+ ...options.profile && { profile: options.profile },
52892
+ ...options.roleArn && { roleArn: options.roleArn }
52893
+ });
52894
+ if (options.retireCfnStack) {
52895
+ logger.info(`[migrate] Retiring source CloudFormation stack '${sourceCfnStackName}'...`);
52896
+ const cfnConfig = {};
52897
+ if (options.region) cfnConfig.region = options.region;
52898
+ if (options.profile) cfnConfig.profile = options.profile;
52899
+ const awsClients = new AwsClients(cfnConfig);
52900
+ const cfnClient = awsClients.cloudFormation;
52901
+ try {
52902
+ await retireCloudFormationStack({
52903
+ cfnStackName: sourceCfnStackName,
52904
+ cfnClient,
52905
+ yes: options.yes ?? false,
52906
+ stateBucket: resolvedStateBucket,
52907
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
52908
+ });
52909
+ } finally {
52910
+ awsClients.destroy();
52911
+ }
52912
+ }
52913
+ logger.info(`[migrate] Migrated ${mappingResult.pairs.length} resource(s) from CloudFormation stack '${sourceCfnStackName}' to cdkd state for stack '${outputStack}'.`);
52914
+ logger.info(`[migrate] Generated CDK app at ${libResult.outputDir}`);
52915
+ 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).`);
52916
+ }
52917
+ /**
52918
+ * Print a tabular `(sourceLogicalId → synthLogicalId physicalId)`
52919
+ * summary so users can see what's about to be imported before the
52920
+ * confirmation prompt. Matches the visual shape of `cdkd import`'s
52921
+ * summary table for consistency.
52922
+ */
52923
+ function printMappingTable(result, sourceStack, outputStack) {
52924
+ const logger = getLogger();
52925
+ logger.info("");
52926
+ logger.info(`[migrate] Resolved mapping (${sourceStack} → ${outputStack}):`);
52927
+ for (const p of result.pairs) logger.info(` ${p.sourceLogicalId} → ${p.synthLogicalId} [${p.resourceType}] ${p.physicalId}`);
52928
+ logger.info("");
52929
+ }
52930
+ /**
52931
+ * Minimal `(y/N)` confirmation prompt using `node:readline`. Mirrors
52932
+ * the shape of [src/cli/commands/import.ts](import.ts)'s `confirmPrompt`
52933
+ * so the user UX is consistent across the import + migrate surfaces.
52934
+ * Non-TTY callers (CI) should pass `--yes` to skip this entirely.
52935
+ */
52936
+ async function promptConfirm(message) {
52937
+ if (!process.stdin.isTTY) throw new LocalMigrateError(`Non-interactive shell detected and --yes was not supplied. Re-run with --yes to confirm: "${message}"`);
52938
+ const rl = (await import("node:readline/promises")).createInterface({
52939
+ input: process.stdin,
52940
+ output: process.stdout
52941
+ });
52942
+ try {
52943
+ const v = (await rl.question(`${message} (y/N) `)).trim().toLowerCase();
52944
+ return v === "y" || v === "yes";
52945
+ } finally {
52946
+ rl.close();
52947
+ }
52948
+ }
52949
+ /**
52950
+ * Commander factory for `cdkd migrate`. Registered in `src/cli/index.ts`
52951
+ * alongside the existing top-level commands. Matches the design doc §3
52952
+ * flag set; the action handler routes through {@link migrateCommandAction}
52953
+ * via `withErrorHandling` so library callers see exceptions but CLI
52954
+ * callers get the standard exit-code-2 routing.
52955
+ */
52956
+ function createMigrateCommand() {
52957
+ 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));
52958
+ }
52959
+
51862
52960
  //#endregion
51863
52961
  //#region src/cli/index.ts
51864
52962
  const SUBCOMMANDS = new Set([
@@ -51876,7 +52974,8 @@ const SUBCOMMANDS = new Set([
51876
52974
  "publish-assets",
51877
52975
  "force-unlock",
51878
52976
  "state",
51879
- "local"
52977
+ "local",
52978
+ "migrate"
51880
52979
  ]);
51881
52980
  /**
51882
52981
  * Reorder args so options before the subcommand are moved after it.
@@ -51900,7 +52999,7 @@ function reorderArgs(argv) {
51900
52999
  */
51901
53000
  async function main() {
51902
53001
  const program = new Command();
51903
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.134.0");
53002
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.135.1");
51904
53003
  program.addCommand(createBootstrapCommand());
51905
53004
  program.addCommand(createSynthCommand());
51906
53005
  program.addCommand(createListCommand());
@@ -51915,6 +53014,7 @@ async function main() {
51915
53014
  program.addCommand(createStateCommand());
51916
53015
  program.addCommand(createLocalCommand());
51917
53016
  program.addCommand(createExportCommand());
53017
+ program.addCommand(createMigrateCommand());
51918
53018
  const args = reorderArgs(process.argv);
51919
53019
  await program.parseAsync(args);
51920
53020
  }