@go-to-k/cdkd 0.83.1 → 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";
@@ -78979,6 +78979,598 @@ function createLocalCommand() {
78979
78979
  return local;
78980
78980
  }
78981
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
+
78982
79574
  // src/cli/index.ts
78983
79575
  var SUBCOMMANDS = /* @__PURE__ */ new Set([
78984
79576
  "bootstrap",
@@ -78991,6 +79583,7 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set([
78991
79583
  "destroy",
78992
79584
  "orphan",
78993
79585
  "import",
79586
+ "export",
78994
79587
  "publish-assets",
78995
79588
  "force-unlock",
78996
79589
  "state",
@@ -79007,8 +79600,8 @@ function reorderArgs(argv) {
79007
79600
  return [...prefix, ...cmdAndAfter, ...beforeCmd];
79008
79601
  }
79009
79602
  async function main() {
79010
- const program = new Command17();
79011
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.83.1");
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");
79012
79605
  program.addCommand(createBootstrapCommand());
79013
79606
  program.addCommand(createSynthCommand());
79014
79607
  program.addCommand(createListCommand());
@@ -79022,6 +79615,7 @@ async function main() {
79022
79615
  program.addCommand(createForceUnlockCommand());
79023
79616
  program.addCommand(createStateCommand());
79024
79617
  program.addCommand(createLocalCommand());
79618
+ program.addCommand(createExportCommand());
79025
79619
  const args = reorderArgs(process.argv);
79026
79620
  await program.parseAsync(args);
79027
79621
  }