@go-to-k/cdkd 0.83.0 → 0.84.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/README.md CHANGED
@@ -395,6 +395,26 @@ modes (auto / selective / hybrid), `--resource-mapping` CDK CLI
395
395
  compatibility, CloudFormation migration flow, provider coverage, and the
396
396
  parity matrix vs upstream `cdk import`.
397
397
 
398
+ ## Exporting a stack back to CloudFormation
399
+
400
+ `cdkd export` is the mirror of `cdkd import`: it hands a cdkd-managed
401
+ stack over to CloudFormation via a CFn `ChangeSetType=IMPORT` changeset.
402
+ AWS resources are unchanged across the migration; cdkd state for the
403
+ exported stack is deleted on success. From then on the stack is managed
404
+ by `cdk deploy` / `aws cloudformation`.
405
+
406
+ ```bash
407
+ cdkd export MyStack # confirmation prompt; CFn stack name = cdkd stack name
408
+ cdkd export MyStack --cfn-stack-name MyStack-CFn
409
+ cdkd export MyStack --dry-run # print the import plan, do not call CFn
410
+ cdkd export MyStack --template path.json # skip synth, use a pre-rendered JSON template
411
+ ```
412
+
413
+ MVP scope: JSON templates only (CDK-generated). The command refuses to
414
+ proceed if any resource is not CFn-importable (Lambda-backed Custom
415
+ Resources, nested `AWS::CloudFormation::Stack` references); destroy or
416
+ accept abandoning those resources first.
417
+
398
418
  ## Drift detection
399
419
 
400
420
  `cdkd drift` (state-driven; no synth) compares each managed resource
package/dist/cli.js CHANGED
@@ -17797,7 +17797,7 @@ var require_graphql2 = __commonJS({
17797
17797
  });
17798
17798
 
17799
17799
  // src/cli/index.ts
17800
- import { Command as Command17 } from "commander";
17800
+ import { Command as Command18 } from "commander";
17801
17801
 
17802
17802
  // src/cli/commands/bootstrap.ts
17803
17803
  import { Command, Option as Option2 } from "commander";
@@ -21471,6 +21471,39 @@ var TemplateParser = class {
21471
21471
  }
21472
21472
  return;
21473
21473
  }
21474
+ if ("Fn::Sub" in obj) {
21475
+ const subValue = obj["Fn::Sub"];
21476
+ let body;
21477
+ let mapKeys;
21478
+ if (typeof subValue === "string") {
21479
+ body = subValue;
21480
+ } else if (Array.isArray(subValue) && subValue.length >= 1 && typeof subValue[0] === "string") {
21481
+ body = subValue[0];
21482
+ const variables = subValue[1];
21483
+ if (variables && typeof variables === "object" && !Array.isArray(variables)) {
21484
+ const varMap = variables;
21485
+ mapKeys = new Set(Object.keys(varMap));
21486
+ Object.values(varMap).forEach((v) => this.extractRefsFromValue(v, dependencies));
21487
+ }
21488
+ }
21489
+ if (body !== void 0) {
21490
+ for (const match of body.matchAll(/\$\{([^}]+)\}/g)) {
21491
+ const placeholder = match[1];
21492
+ if (!placeholder)
21493
+ continue;
21494
+ const dot = placeholder.indexOf(".");
21495
+ const name = dot >= 0 ? placeholder.slice(0, dot) : placeholder;
21496
+ if (!name)
21497
+ continue;
21498
+ if (name.startsWith("AWS::"))
21499
+ continue;
21500
+ if (mapKeys?.has(name))
21501
+ continue;
21502
+ dependencies.add(name);
21503
+ }
21504
+ }
21505
+ return;
21506
+ }
21474
21507
  Object.values(obj).forEach((v) => this.extractRefsFromValue(v, dependencies));
21475
21508
  }
21476
21509
  /**
@@ -78946,6 +78979,598 @@ function createLocalCommand() {
78946
78979
  return local;
78947
78980
  }
78948
78981
 
78982
+ // src/cli/commands/export.ts
78983
+ import * as readline8 from "node:readline/promises";
78984
+ import { readFileSync as readFileSync9 } from "node:fs";
78985
+ import { Command as Command17 } from "commander";
78986
+ import {
78987
+ CreateChangeSetCommand,
78988
+ DescribeChangeSetCommand,
78989
+ ExecuteChangeSetCommand,
78990
+ DescribeStacksCommand as DescribeStacksCommand2,
78991
+ DescribeTypeCommand,
78992
+ DeleteChangeSetCommand,
78993
+ waitUntilChangeSetCreateComplete,
78994
+ waitUntilStackImportComplete
78995
+ } from "@aws-sdk/client-cloudformation";
78996
+ init_aws_clients();
78997
+ var NEVER_IMPORTABLE_TYPES = /* @__PURE__ */ new Set([
78998
+ "AWS::CDK::Metadata",
78999
+ "AWS::CloudFormation::Stack"
79000
+ ]);
79001
+ function isNeverImportableType(resourceType) {
79002
+ if (NEVER_IMPORTABLE_TYPES.has(resourceType))
79003
+ return true;
79004
+ if (resourceType.startsWith("Custom::"))
79005
+ return true;
79006
+ return false;
79007
+ }
79008
+ var PRIMARY_IDENTIFIER_FALLBACK = {
79009
+ "AWS::S3::Bucket": "BucketName",
79010
+ "AWS::IAM::Role": "RoleName",
79011
+ "AWS::IAM::ManagedPolicy": "PolicyArn",
79012
+ "AWS::IAM::User": "UserName",
79013
+ "AWS::IAM::Group": "GroupName",
79014
+ "AWS::IAM::InstanceProfile": "InstanceProfileName",
79015
+ "AWS::Lambda::Function": "FunctionName",
79016
+ "AWS::DynamoDB::Table": "TableName",
79017
+ "AWS::SQS::Queue": "QueueUrl",
79018
+ "AWS::SNS::Topic": "TopicArn",
79019
+ "AWS::Logs::LogGroup": "LogGroupName",
79020
+ "AWS::EC2::VPC": "VpcId",
79021
+ "AWS::EC2::Subnet": "SubnetId",
79022
+ "AWS::EC2::SecurityGroup": "GroupId",
79023
+ "AWS::EC2::InternetGateway": "InternetGatewayId",
79024
+ "AWS::EC2::RouteTable": "RouteTableId",
79025
+ "AWS::EC2::NatGateway": "NatGatewayId",
79026
+ "AWS::CloudFront::Distribution": "Id",
79027
+ "AWS::CloudFront::CloudFrontOriginAccessIdentity": "Id",
79028
+ "AWS::Route53::HostedZone": "Id",
79029
+ "AWS::SecretsManager::Secret": "Id",
79030
+ "AWS::Events::Rule": "Arn",
79031
+ "AWS::Events::EventBus": "Name",
79032
+ "AWS::ApiGateway::RestApi": "RestApiId",
79033
+ "AWS::ApiGatewayV2::Api": "ApiId",
79034
+ "AWS::CloudWatch::Alarm": "AlarmName",
79035
+ "AWS::Kinesis::Stream": "Name",
79036
+ "AWS::SSM::Parameter": "Name",
79037
+ "AWS::StepFunctions::StateMachine": "Arn",
79038
+ "AWS::Cognito::UserPool": "UserPoolId",
79039
+ "AWS::ECR::Repository": "RepositoryName"
79040
+ };
79041
+ async function exportCommand(stackArg, options) {
79042
+ const logger = getLogger();
79043
+ if (options.verbose) {
79044
+ logger.setLevel("debug");
79045
+ process.env["CDKD_NO_LIVE"] = "1";
79046
+ }
79047
+ warnIfDeprecatedRegion(options);
79048
+ refuseTransientContextIfUnsafe(options);
79049
+ await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
79050
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
79051
+ const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
79052
+ if (options.region) {
79053
+ process.env["AWS_REGION"] = options.region;
79054
+ process.env["AWS_DEFAULT_REGION"] = options.region;
79055
+ }
79056
+ const awsClients = new AwsClients({
79057
+ ...options.region && { region: options.region },
79058
+ ...options.profile && { profile: options.profile }
79059
+ });
79060
+ setAwsClients(awsClients);
79061
+ try {
79062
+ const stateConfig = { bucket: stateBucket, prefix: options.statePrefix };
79063
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
79064
+ ...options.region && { region: options.region },
79065
+ ...options.profile && { profile: options.profile }
79066
+ });
79067
+ await stateBackend.verifyBucketExists();
79068
+ const lockManager = new LockManager(awsClients.s3, stateConfig);
79069
+ let template;
79070
+ let resolvedStackName;
79071
+ let synthedRegion;
79072
+ if (options.template) {
79073
+ if (!stackArg) {
79074
+ throw new Error(
79075
+ "--template requires a stack name as a positional argument to identify the cdkd state record."
79076
+ );
79077
+ }
79078
+ template = parseTemplateFile(options.template);
79079
+ resolvedStackName = stackArg;
79080
+ } else {
79081
+ const appCmd = options.app || resolveApp();
79082
+ if (!appCmd) {
79083
+ throw new Error(
79084
+ "'cdkd export' requires a CDK app (pass --app or set it in cdk.json) OR a pre-rendered CFn template (--template <path>)."
79085
+ );
79086
+ }
79087
+ logger.info("Synthesizing CDK app to read template...");
79088
+ const synthesizer = new Synthesizer();
79089
+ const context = parseContextOptions(options.context);
79090
+ const result = await synthesizer.synthesize({
79091
+ app: appCmd,
79092
+ output: options.output || "cdk.out",
79093
+ ...Object.keys(context).length > 0 && { context }
79094
+ });
79095
+ let stackInfo;
79096
+ if (stackArg) {
79097
+ stackInfo = result.stacks.find(
79098
+ (s) => s.stackName === stackArg || s.displayName === stackArg
79099
+ );
79100
+ if (!stackInfo) {
79101
+ throw new Error(
79102
+ `Stack '${stackArg}' not found in synthesized app. Available: ${result.stacks.map((s) => s.stackName).join(", ")}`
79103
+ );
79104
+ }
79105
+ } else if (result.stacks.length === 1) {
79106
+ stackInfo = result.stacks[0];
79107
+ } else {
79108
+ throw new Error(
79109
+ `Multiple stacks found: ${result.stacks.map((s) => s.stackName).join(", ")}. Specify the stack name as a positional argument.`
79110
+ );
79111
+ }
79112
+ template = stackInfo.template;
79113
+ resolvedStackName = stackInfo.stackName;
79114
+ synthedRegion = stackInfo.region;
79115
+ }
79116
+ const cfnStackName = options.cfnStackName ?? resolvedStackName;
79117
+ const targetRegion = await pickStackRegion2(
79118
+ stateBackend,
79119
+ resolvedStackName,
79120
+ synthedRegion,
79121
+ options.stackRegion
79122
+ );
79123
+ logger.info(
79124
+ `Migrating cdkd stack '${resolvedStackName}' (${targetRegion}) \u2192 CloudFormation stack '${cfnStackName}'`
79125
+ );
79126
+ await assertCfnStackAbsent(awsClients.cloudFormation, cfnStackName);
79127
+ const stateData = await stateBackend.getState(resolvedStackName, targetRegion);
79128
+ if (!stateData) {
79129
+ throw new Error(
79130
+ `No cdkd state found for stack '${resolvedStackName}' (${targetRegion}). Nothing to migrate.`
79131
+ );
79132
+ }
79133
+ const { state, etag, migrationPending } = stateData;
79134
+ const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
79135
+ if (!options.dryRun) {
79136
+ const acquired = await lockManager.acquireLock(
79137
+ resolvedStackName,
79138
+ targetRegion,
79139
+ owner,
79140
+ "export"
79141
+ );
79142
+ if (!acquired) {
79143
+ throw new Error(
79144
+ `Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) \u2014 another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`
79145
+ );
79146
+ }
79147
+ }
79148
+ try {
79149
+ const { plan, skipped } = await buildImportPlan(state, template, awsClients.cloudFormation);
79150
+ if (skipped.length > 0) {
79151
+ logger.error("The following resources cannot be imported into CloudFormation:");
79152
+ for (const s of skipped) {
79153
+ logger.error(` - ${s.logicalId} (${s.resourceType}): ${s.reason}`);
79154
+ }
79155
+ throw new Error(
79156
+ `${skipped.length} resource(s) cannot be imported. CloudFormation IMPORT requires every template resource to map to an importable AWS resource. Either destroy these resources first (cdkd destroy / cdkd state destroy cherry-picked), or accept abandoning them by removing them from the CDK app and re-synthesizing.`
79157
+ );
79158
+ }
79159
+ if (plan.length === 0) {
79160
+ logger.warn("No resources to import \u2014 cdkd state is empty.");
79161
+ return;
79162
+ }
79163
+ printPlan(plan, cfnStackName);
79164
+ if (options.dryRun) {
79165
+ logger.info("--dry-run: no CloudFormation changeset will be created.");
79166
+ return;
79167
+ }
79168
+ if (!options.yes) {
79169
+ const ok = await confirmPrompt6(
79170
+ `Create CloudFormation stack '${cfnStackName}' by importing ${plan.length} resource(s) from cdkd state '${resolvedStackName}' (${targetRegion})? AWS resources are unchanged. cdkd state for '${resolvedStackName}' will be deleted on success.`
79171
+ );
79172
+ if (!ok) {
79173
+ logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
79174
+ return;
79175
+ }
79176
+ }
79177
+ const filteredTemplate = filterTemplateForImport(template, plan);
79178
+ await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, filteredTemplate, plan);
79179
+ logger.info(
79180
+ `\u2713 CloudFormation stack '${cfnStackName}' created via IMPORT. ${plan.length} resource(s) are now managed by CloudFormation.`
79181
+ );
79182
+ await stateBackend.deleteState(resolvedStackName, targetRegion);
79183
+ logger.info(
79184
+ `cdkd state for '${resolvedStackName}' (${targetRegion}) removed. Manage the stack with 'cdk deploy' or 'aws cloudformation' from here on.`
79185
+ );
79186
+ printNextSteps({
79187
+ cfnStackName,
79188
+ cdkStackName: resolvedStackName,
79189
+ contextOverrides: options.context ?? []
79190
+ });
79191
+ } finally {
79192
+ if (!options.dryRun) {
79193
+ await lockManager.releaseLock(resolvedStackName, targetRegion).catch((err) => {
79194
+ logger.warn(
79195
+ `Failed to release lock: ${err instanceof Error ? err.message : String(err)}`
79196
+ );
79197
+ });
79198
+ }
79199
+ }
79200
+ } finally {
79201
+ awsClients.destroy();
79202
+ }
79203
+ }
79204
+ async function pickStackRegion2(stateBackend, stackName, synthRegion, flag) {
79205
+ const refs = (await stateBackend.listStacks()).filter((r) => r.stackName === stackName);
79206
+ if (refs.length === 0) {
79207
+ if (flag)
79208
+ return flag;
79209
+ if (synthRegion)
79210
+ return synthRegion;
79211
+ throw new Error(
79212
+ `No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
79213
+ );
79214
+ }
79215
+ if (flag) {
79216
+ const found = refs.find((r) => r.region === flag);
79217
+ if (!found) {
79218
+ const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
79219
+ throw new Error(
79220
+ `No state found for stack '${stackName}' in region '${flag}'. Available regions: ${seen}.`
79221
+ );
79222
+ }
79223
+ return flag;
79224
+ }
79225
+ if (synthRegion) {
79226
+ const found = refs.find((r) => r.region === synthRegion);
79227
+ if (found)
79228
+ return synthRegion;
79229
+ }
79230
+ if (refs.length === 1) {
79231
+ return refs[0].region ?? synthRegion ?? "";
79232
+ }
79233
+ const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
79234
+ throw new Error(
79235
+ `Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`
79236
+ );
79237
+ }
79238
+ function parseTemplateFile(path3) {
79239
+ let raw;
79240
+ try {
79241
+ raw = readFileSync9(path3, "utf-8");
79242
+ } catch (err) {
79243
+ throw new Error(
79244
+ `Failed to read template file '${path3}': ` + (err instanceof Error ? err.message : String(err))
79245
+ );
79246
+ }
79247
+ let parsed;
79248
+ try {
79249
+ parsed = JSON.parse(raw);
79250
+ } catch (err) {
79251
+ throw new Error(
79252
+ `Template file '${path3}' is not valid JSON. cdkd export only supports JSON templates (CDK-generated). Cause: ` + (err instanceof Error ? err.message : String(err))
79253
+ );
79254
+ }
79255
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
79256
+ throw new Error(`Template file '${path3}' is not a JSON object.`);
79257
+ }
79258
+ return parsed;
79259
+ }
79260
+ async function assertCfnStackAbsent(cfnClient, stackName) {
79261
+ try {
79262
+ const resp = await cfnClient.send(new DescribeStacksCommand2({ StackName: stackName }));
79263
+ const stack = resp.Stacks?.[0];
79264
+ if (!stack)
79265
+ return;
79266
+ throw new Error(
79267
+ `CloudFormation stack '${stackName}' already exists (status: ${stack.StackStatus ?? "unknown"}). cdkd export only creates new stacks via IMPORT \u2014 delete or rename the existing stack first, or pass --cfn-stack-name to choose a different name.`
79268
+ );
79269
+ } catch (err) {
79270
+ const msg = err instanceof Error ? err.message : String(err);
79271
+ if (/does not exist/i.test(msg)) {
79272
+ return;
79273
+ }
79274
+ throw err;
79275
+ }
79276
+ }
79277
+ async function buildImportPlan(state, template, cfnClient) {
79278
+ const templateResources = template["Resources"];
79279
+ if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) {
79280
+ throw new Error("Template has no Resources section.");
79281
+ }
79282
+ const plan = [];
79283
+ const skipped = [];
79284
+ const identifierCache = /* @__PURE__ */ new Map();
79285
+ for (const [logicalId, raw] of Object.entries(templateResources)) {
79286
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
79287
+ continue;
79288
+ const resource = raw;
79289
+ const resourceType = resource.Type ?? "";
79290
+ if (!resourceType)
79291
+ continue;
79292
+ if (isNeverImportableType(resourceType)) {
79293
+ if (resourceType === "AWS::CDK::Metadata")
79294
+ continue;
79295
+ skipped.push({
79296
+ logicalId,
79297
+ resourceType,
79298
+ reason: "CloudFormation IMPORT does not support this resource type"
79299
+ });
79300
+ continue;
79301
+ }
79302
+ const stateEntry = state.resources[logicalId];
79303
+ if (!stateEntry || !stateEntry.physicalId) {
79304
+ skipped.push({
79305
+ logicalId,
79306
+ resourceType,
79307
+ reason: "no entry in cdkd state (resource is in template but was not deployed by cdkd)"
79308
+ });
79309
+ continue;
79310
+ }
79311
+ let identifierKey;
79312
+ try {
79313
+ identifierKey = await resolvePrimaryIdentifier(resourceType, cfnClient, identifierCache);
79314
+ } catch (err) {
79315
+ skipped.push({
79316
+ logicalId,
79317
+ resourceType,
79318
+ reason: "could not resolve primary identifier: " + (err instanceof Error ? err.message : String(err))
79319
+ });
79320
+ continue;
79321
+ }
79322
+ plan.push({
79323
+ logicalId,
79324
+ resourceType,
79325
+ physicalId: stateEntry.physicalId,
79326
+ identifierKey
79327
+ });
79328
+ }
79329
+ return { plan, skipped };
79330
+ }
79331
+ async function resolvePrimaryIdentifier(resourceType, cfnClient, cache2) {
79332
+ const cached = cache2.get(resourceType);
79333
+ if (cached !== void 0)
79334
+ return cached;
79335
+ try {
79336
+ const resp = await cfnClient.send(
79337
+ new DescribeTypeCommand({ Type: "RESOURCE", TypeName: resourceType })
79338
+ );
79339
+ if (resp.Schema) {
79340
+ const parsed = JSON.parse(resp.Schema);
79341
+ const primary = parsed.primaryIdentifier;
79342
+ if (Array.isArray(primary) && primary.length === 1 && typeof primary[0] === "string") {
79343
+ const propName = primary[0].replace(/^\/properties\//, "");
79344
+ cache2.set(resourceType, propName);
79345
+ return propName;
79346
+ }
79347
+ if (Array.isArray(primary) && primary.length > 1) {
79348
+ throw new Error(
79349
+ `resource type uses a composite primary identifier (${primary.length} fields); cdkd does not yet support composite identifiers for cdkd export`
79350
+ );
79351
+ }
79352
+ }
79353
+ } catch (err) {
79354
+ const msg = err instanceof Error ? err.message : String(err);
79355
+ getLogger().debug(`DescribeType failed for ${resourceType}: ${msg} \u2014 using fallback`);
79356
+ }
79357
+ const fallback = PRIMARY_IDENTIFIER_FALLBACK[resourceType];
79358
+ if (fallback) {
79359
+ cache2.set(resourceType, fallback);
79360
+ return fallback;
79361
+ }
79362
+ throw new Error(
79363
+ `primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`
79364
+ );
79365
+ }
79366
+ function filterTemplateForImport(template, plan) {
79367
+ const allow = new Set(plan.map((p) => p.logicalId));
79368
+ const original = template["Resources"];
79369
+ const filteredResources = {};
79370
+ for (const [logicalId, resource] of Object.entries(original)) {
79371
+ if (allow.has(logicalId)) {
79372
+ filteredResources[logicalId] = resource;
79373
+ }
79374
+ }
79375
+ const result = { ...template, Resources: filteredResources };
79376
+ const outputs = template["Outputs"];
79377
+ if (outputs && typeof outputs === "object" && !Array.isArray(outputs)) {
79378
+ const filteredOutputs = {};
79379
+ for (const [name, output] of Object.entries(outputs)) {
79380
+ if (referencesOnly(output, allow)) {
79381
+ filteredOutputs[name] = output;
79382
+ }
79383
+ }
79384
+ if (Object.keys(filteredOutputs).length > 0) {
79385
+ result["Outputs"] = filteredOutputs;
79386
+ } else {
79387
+ delete result["Outputs"];
79388
+ }
79389
+ }
79390
+ return result;
79391
+ }
79392
+ function referencesOnly(node, allow) {
79393
+ if (!node || typeof node !== "object")
79394
+ return true;
79395
+ if (Array.isArray(node)) {
79396
+ return node.every((item) => referencesOnly(item, allow));
79397
+ }
79398
+ for (const [key, value] of Object.entries(node)) {
79399
+ if (key === "Ref" && typeof value === "string") {
79400
+ if (!allow.has(value))
79401
+ return false;
79402
+ continue;
79403
+ }
79404
+ if (key === "Fn::GetAtt") {
79405
+ const target = Array.isArray(value) && typeof value[0] === "string" ? value[0] : typeof value === "string" ? value.split(".")[0] : void 0;
79406
+ if (target && !allow.has(target))
79407
+ return false;
79408
+ continue;
79409
+ }
79410
+ if (!referencesOnly(value, allow))
79411
+ return false;
79412
+ }
79413
+ return true;
79414
+ }
79415
+ function printPlan(plan, cfnStackName) {
79416
+ const logger = getLogger();
79417
+ logger.info("");
79418
+ logger.info(`Import plan for CloudFormation stack '${cfnStackName}':`);
79419
+ for (const entry of plan) {
79420
+ logger.info(
79421
+ ` ${entry.logicalId} (${entry.resourceType}) \u2190 ${entry.identifierKey}=${entry.physicalId}`
79422
+ );
79423
+ }
79424
+ logger.info("");
79425
+ }
79426
+ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79427
+ const logger = getLogger();
79428
+ const changeSetName = `cdkd-migrate-${Date.now()}`;
79429
+ const templateBody = JSON.stringify(template, null, 2);
79430
+ const resourcesToImport = plan.map((entry) => ({
79431
+ ResourceType: entry.resourceType,
79432
+ LogicalResourceId: entry.logicalId,
79433
+ ResourceIdentifier: { [entry.identifierKey]: entry.physicalId }
79434
+ }));
79435
+ logger.info(
79436
+ `Creating IMPORT changeset '${changeSetName}' for stack '${stackName}' (${plan.length} resource(s), ${templateBody.length} bytes)...`
79437
+ );
79438
+ if (templateBody.length > 51200) {
79439
+ throw new Error(
79440
+ `Filtered template is ${templateBody.length} bytes, over the 51,200-byte inline TemplateBody limit. Templates that large require TemplateURL upload (not yet implemented for cdkd export; please file an issue if you hit this).`
79441
+ );
79442
+ }
79443
+ try {
79444
+ await cfnClient.send(
79445
+ new CreateChangeSetCommand({
79446
+ StackName: stackName,
79447
+ ChangeSetName: changeSetName,
79448
+ ChangeSetType: "IMPORT",
79449
+ TemplateBody: templateBody,
79450
+ ResourcesToImport: resourcesToImport,
79451
+ // CDK templates routinely require CAPABILITY_IAM /
79452
+ // CAPABILITY_NAMED_IAM. Forward both so the user does not have to
79453
+ // re-discover and re-pass them.
79454
+ Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
79455
+ })
79456
+ );
79457
+ } catch (err) {
79458
+ const msg = err instanceof Error ? err.message : String(err);
79459
+ throw new Error(`Failed to create IMPORT changeset: ${msg}`);
79460
+ }
79461
+ try {
79462
+ await waitUntilChangeSetCreateComplete(
79463
+ { client: cfnClient, maxWaitTime: 600 },
79464
+ { StackName: stackName, ChangeSetName: changeSetName }
79465
+ );
79466
+ } catch (err) {
79467
+ try {
79468
+ const desc = await cfnClient.send(
79469
+ new DescribeChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })
79470
+ );
79471
+ const reason = desc.StatusReason ?? "unknown";
79472
+ await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
79473
+ });
79474
+ throw new Error(`IMPORT changeset FAILED: ${reason}`);
79475
+ } catch (innerErr) {
79476
+ if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) {
79477
+ throw innerErr;
79478
+ }
79479
+ throw err;
79480
+ }
79481
+ }
79482
+ logger.info(`Executing IMPORT changeset...`);
79483
+ try {
79484
+ await cfnClient.send(
79485
+ new ExecuteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })
79486
+ );
79487
+ await waitUntilStackImportComplete(
79488
+ { client: cfnClient, maxWaitTime: 3600 },
79489
+ { StackName: stackName }
79490
+ );
79491
+ } catch (err) {
79492
+ await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
79493
+ });
79494
+ throw err;
79495
+ }
79496
+ }
79497
+ function refuseTransientContextIfUnsafe(options) {
79498
+ const overrides = options.context ?? [];
79499
+ if (overrides.length === 0)
79500
+ return;
79501
+ if (!options.acceptTransientContext) {
79502
+ const indented = overrides.map((v) => ` -c ${v}`).join("\n");
79503
+ throw new Error(
79504
+ `Refusing to export: ${overrides.length} CLI context override(s) supplied via -c are not persisted to cdk.json / cdk.context.json, so subsequent \`cdk deploy\` invocations will synthesize a different template and CFn will see drift or replace resources.
79505
+
79506
+ Supplied:
79507
+ ${indented}
79508
+
79509
+ Choose one:
79510
+ (recommended) Move these values into cdk.json's "context": { ... } field, then re-run
79511
+ cdkd export without -c. CDK CLI reads cdk.json on every synth, so they
79512
+ will be picked up automatically.
79513
+ (escape) Pass --accept-transient-context to proceed. You will then need to
79514
+ pass the SAME -c flags to every future \`cdk deploy\` for this stack.`
79515
+ );
79516
+ }
79517
+ const logger = getLogger();
79518
+ logger.warn(
79519
+ `--accept-transient-context: ${overrides.length} CLI context override(s) will not be persisted to cdk.json / cdk.context.json. Remember to pass the same -c flags to every future \`cdk deploy\` for this stack, or move them to cdk.json before then.`
79520
+ );
79521
+ for (const v of overrides) {
79522
+ logger.warn(` -c ${v}`);
79523
+ }
79524
+ }
79525
+ function printNextSteps(args) {
79526
+ const logger = getLogger();
79527
+ const ctxArgs = args.contextOverrides.map((v) => ` -c ${v}`).join("");
79528
+ const stackId = args.cdkStackName;
79529
+ logger.info("");
79530
+ logger.info("Next steps \u2014 manage the stack with CDK CLI from now on:");
79531
+ logger.info(` cdk diff ${stackId}${ctxArgs} # verify synth matches what CFn now holds`);
79532
+ logger.info(` cdk deploy ${stackId}${ctxArgs} # subsequent updates`);
79533
+ if (args.contextOverrides.length > 0) {
79534
+ logger.info("");
79535
+ logger.info(
79536
+ ` NOTE: the -c flags above were captured from this export run. They MUST be passed on every future cdk invocation, or moved into cdk.json's "context" field.`
79537
+ );
79538
+ }
79539
+ logger.info("");
79540
+ }
79541
+ async function confirmPrompt6(prompt) {
79542
+ const rl = readline8.createInterface({ input: process.stdin, output: process.stdout });
79543
+ try {
79544
+ const ans = await rl.question(`${prompt} [y/N] `);
79545
+ return /^y(es)?$/i.test(ans.trim());
79546
+ } finally {
79547
+ rl.close();
79548
+ }
79549
+ }
79550
+ function createExportCommand() {
79551
+ const cmd = new Command17("export").description(
79552
+ "Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS \u2192 cdkd) in the reverse direction (cdkd \u2192 CFn). JSON templates only. Aborts if any resource is not CFn-importable."
79553
+ ).argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option(
79554
+ "--cfn-stack-name <name>",
79555
+ "Name of the destination CloudFormation stack. Defaults to the cdkd stack name."
79556
+ ).option(
79557
+ "--template <path>",
79558
+ "Path to a pre-rendered CloudFormation template (JSON). Skips synth."
79559
+ ).option(
79560
+ "--stack-region <region>",
79561
+ "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions."
79562
+ ).option("--dry-run", "Print the import plan without creating a changeset.", false).option(
79563
+ "--accept-transient-context",
79564
+ "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.",
79565
+ false
79566
+ ).action(withErrorHandling(exportCommand));
79567
+ [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
79568
+ (opt) => cmd.addOption(opt)
79569
+ );
79570
+ cmd.addOption(deprecatedRegionOption);
79571
+ return cmd;
79572
+ }
79573
+
78949
79574
  // src/cli/index.ts
78950
79575
  var SUBCOMMANDS = /* @__PURE__ */ new Set([
78951
79576
  "bootstrap",
@@ -78958,6 +79583,7 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
78958
79583
  "destroy",
78959
79584
  "orphan",
78960
79585
  "import",
79586
+ "export",
78961
79587
  "publish-assets",
78962
79588
  "force-unlock",
78963
79589
  "state",
@@ -78974,8 +79600,8 @@ function reorderArgs(argv) {
78974
79600
  return [...prefix, ...cmdAndAfter, ...beforeCmd];
78975
79601
  }
78976
79602
  async function main() {
78977
- const program = new Command17();
78978
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.83.0");
79603
+ const program = new Command18();
79604
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.84.0");
78979
79605
  program.addCommand(createBootstrapCommand());
78980
79606
  program.addCommand(createSynthCommand());
78981
79607
  program.addCommand(createListCommand());
@@ -78989,6 +79615,7 @@ async function main() {
78989
79615
  program.addCommand(createForceUnlockCommand());
78990
79616
  program.addCommand(createStateCommand());
78991
79617
  program.addCommand(createLocalCommand());
79618
+ program.addCommand(createExportCommand());
78992
79619
  const args = reorderArgs(process.argv);
78993
79620
  await program.parseAsync(args);
78994
79621
  }