@go-to-k/cdkd 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -450,7 +450,7 @@ var init_aws_clients = __esm({
450
450
  import { Command as Command10 } from "commander";
451
451
 
452
452
  // src/cli/commands/bootstrap.ts
453
- import { Command } from "commander";
453
+ import { Command, Option as Option2 } from "commander";
454
454
  import {
455
455
  CreateBucketCommand,
456
456
  HeadBucketCommand,
@@ -476,13 +476,23 @@ function parseContextOptions(contextArgs) {
476
476
  }
477
477
  var commonOptions = [
478
478
  new Option("--verbose", "Enable verbose logging").default(false),
479
- new Option("--region <region>", "AWS region"),
480
479
  new Option("--profile <profile>", "AWS profile"),
481
480
  new Option(
482
481
  "-y, --yes",
483
482
  "Automatically answer interactive prompts with the recommended response (e.g. confirm destroy)"
484
483
  ).default(false)
485
484
  ];
485
+ var deprecatedRegionOption = new Option(
486
+ "--region <region>",
487
+ "[deprecated] No effect on this command; use AWS_REGION or your AWS profile"
488
+ ).hideHelp();
489
+ function warnIfDeprecatedRegion(options) {
490
+ if (options.region !== void 0) {
491
+ process.stderr.write(
492
+ "Warning: --region is deprecated for this command and has no effect. Use the AWS_REGION environment variable or your AWS profile to override the SDK default region.\n"
493
+ );
494
+ }
495
+ }
486
496
  var appOptions = [
487
497
  new Option(
488
498
  "-a, --app <command>",
@@ -971,7 +981,10 @@ function resolveStateBucket(cliBucket) {
971
981
  const bucket = cdkdContext?.["stateBucket"];
972
982
  return typeof bucket === "string" ? bucket : void 0;
973
983
  }
974
- function getDefaultStateBucketName(accountId, region) {
984
+ function getDefaultStateBucketName(accountId) {
985
+ return `cdkd-state-${accountId}`;
986
+ }
987
+ function getLegacyStateBucketName(accountId, region) {
975
988
  return `cdkd-state-${accountId}-${region}`;
976
989
  }
977
990
  async function resolveStateBucketWithDefault(cliBucket, region) {
@@ -981,13 +994,48 @@ async function resolveStateBucketWithDefault(cliBucket, region) {
981
994
  const logger = getLogger();
982
995
  logger.debug("No state bucket specified, resolving default from account...");
983
996
  const { GetCallerIdentityCommand: GetCallerIdentityCommand8 } = await import("@aws-sdk/client-sts");
997
+ const { S3Client: S3Client10 } = await import("@aws-sdk/client-s3");
984
998
  const { getAwsClients: getAwsClients2 } = await Promise.resolve().then(() => (init_aws_clients(), aws_clients_exports));
985
999
  const awsClients = getAwsClients2();
986
1000
  const identity = await awsClients.sts.send(new GetCallerIdentityCommand8({}));
987
1001
  const accountId = identity.Account;
988
- const bucketName = getDefaultStateBucketName(accountId, region);
989
- logger.info(`State bucket: ${bucketName}`);
990
- return bucketName;
1002
+ const newName = getDefaultStateBucketName(accountId);
1003
+ const legacyName = getLegacyStateBucketName(accountId, region);
1004
+ const probe = new S3Client10({ region: "us-east-1" });
1005
+ try {
1006
+ if (await bucketExists(probe, newName)) {
1007
+ logger.info(`State bucket: ${newName}`);
1008
+ return newName;
1009
+ }
1010
+ if (await bucketExists(probe, legacyName)) {
1011
+ logger.warn(
1012
+ `Using legacy state bucket name '${legacyName}'. The default has changed to '${newName}'. Future cdkd versions will drop legacy support; consider migrating with cdkd state migrate-bucket (coming in a future release).`
1013
+ );
1014
+ return legacyName;
1015
+ }
1016
+ throw new Error(
1017
+ `No cdkd state bucket found for account ${accountId}. Looked for '${newName}' (current default) and '${legacyName}' (legacy default). Run 'cdkd bootstrap' to create '${newName}'.`
1018
+ );
1019
+ } finally {
1020
+ probe.destroy();
1021
+ }
1022
+ }
1023
+ async function bucketExists(client, bucketName) {
1024
+ const { HeadBucketCommand: HeadBucketCommand3 } = await import("@aws-sdk/client-s3");
1025
+ try {
1026
+ await client.send(new HeadBucketCommand3({ Bucket: bucketName }));
1027
+ return true;
1028
+ } catch (error) {
1029
+ const err = error;
1030
+ const status = err.$metadata?.httpStatusCode;
1031
+ if (err.name === "NotFound" || err.name === "NoSuchBucket" || status === 404) {
1032
+ return false;
1033
+ }
1034
+ if (status === 301 || status === 403) {
1035
+ return true;
1036
+ }
1037
+ throw error;
1038
+ }
991
1039
  }
992
1040
 
993
1041
  // src/cli/commands/bootstrap.ts
@@ -1015,14 +1063,14 @@ async function bootstrapCommand(options) {
1015
1063
  logger.info("No --state-bucket specified, resolving default bucket name...");
1016
1064
  const identity = await awsClients.sts.send(new GetCallerIdentityCommand({}));
1017
1065
  accountId = identity.Account;
1018
- bucketName = getDefaultStateBucketName(accountId, region);
1066
+ bucketName = getDefaultStateBucketName(accountId);
1019
1067
  logger.info(`Using default state bucket: ${bucketName}`);
1020
1068
  }
1021
1069
  try {
1022
- let bucketExists = false;
1070
+ let bucketExists2 = false;
1023
1071
  try {
1024
1072
  await s3Client.send(new HeadBucketCommand({ Bucket: bucketName }));
1025
- bucketExists = true;
1073
+ bucketExists2 = true;
1026
1074
  logger.info(`Bucket ${bucketName} already exists`);
1027
1075
  } catch (error) {
1028
1076
  const err = error;
@@ -1032,7 +1080,7 @@ async function bootstrapCommand(options) {
1032
1080
  throw normalizeAwsError(error, { bucket: bucketName, operation: "HeadBucket" });
1033
1081
  }
1034
1082
  }
1035
- if (bucketExists) {
1083
+ if (bucketExists2) {
1036
1084
  if (!options.force) {
1037
1085
  logger.warn(
1038
1086
  `Bucket ${bucketName} already exists. Use --force to reconfigure (this will not delete existing state)`
@@ -1119,8 +1167,17 @@ State bucket: ${bucketName}`);
1119
1167
  function createBootstrapCommand() {
1120
1168
  const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating required S3 bucket for state management").option(
1121
1169
  "--state-bucket <bucket>",
1122
- "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId}-{region})"
1123
- ).option("--force", "Force reconfiguration of existing bucket", false).action(withErrorHandling(bootstrapCommand));
1170
+ "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})"
1171
+ ).option("--force", "Force reconfiguration of existing bucket", false).addOption(
1172
+ // Bootstrap-specific: needs to know which region to create the bucket
1173
+ // in. After PR 5, `--region` is removed from `commonOptions` and only
1174
+ // re-added explicitly here — every other command resolves the region
1175
+ // from `AWS_REGION` / profile.
1176
+ new Option2(
1177
+ "--region <region>",
1178
+ "AWS region in which to create the state bucket (defaults to AWS_REGION env or us-east-1)"
1179
+ )
1180
+ ).action(withErrorHandling(bootstrapCommand));
1124
1181
  commonOptions.forEach((opt) => cmd.addOption(opt));
1125
1182
  return cmd;
1126
1183
  }
@@ -2455,6 +2512,7 @@ async function synthCommand(options) {
2455
2512
  if (options.verbose) {
2456
2513
  logger.setLevel("debug");
2457
2514
  }
2515
+ warnIfDeprecatedRegion(options);
2458
2516
  const app = resolveApp(options.app);
2459
2517
  if (!app) {
2460
2518
  throw new Error(
@@ -2502,6 +2560,7 @@ Output: ${assemblyDir}`);
2502
2560
  function createSynthCommand() {
2503
2561
  const cmd = new Command2("synth").description("Synthesize CDK app to CloudFormation template").action(withErrorHandling(synthCommand));
2504
2562
  [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
2563
+ cmd.addOption(deprecatedRegionOption);
2505
2564
  return cmd;
2506
2565
  }
2507
2566
 
@@ -2584,6 +2643,7 @@ async function listCommand(patterns, options) {
2584
2643
  if (options.verbose) {
2585
2644
  logger.setLevel("debug");
2586
2645
  }
2646
+ warnIfDeprecatedRegion(options);
2587
2647
  const app = resolveApp(options.app);
2588
2648
  if (!app) {
2589
2649
  throw new Error(
@@ -2649,6 +2709,7 @@ function createListCommand() {
2649
2709
  "Stack name pattern(s). Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*')."
2650
2710
  ).option("-l, --long", "Display environment information for each stack", false).option("-d, --show-dependencies", "Display stack dependency information for each stack", false).option("--json", "Output as JSON instead of YAML for --long / --show-dependencies", false).action(withErrorHandling(listCommand));
2651
2711
  [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
2712
+ cmd.addOption(deprecatedRegionOption);
2652
2713
  return cmd;
2653
2714
  }
2654
2715
 
@@ -28548,6 +28609,7 @@ async function deployCommand(stacks, options) {
28548
28609
  logger.setLevel("debug");
28549
28610
  process.env["CDKD_NO_LIVE"] = "1";
28550
28611
  }
28612
+ warnIfDeprecatedRegion(options);
28551
28613
  if (!options.wait) {
28552
28614
  process.env["CDKD_NO_WAIT"] = "true";
28553
28615
  }
@@ -28797,6 +28859,7 @@ function createDeployCommand() {
28797
28859
  ...deployOptions,
28798
28860
  ...contextOptions
28799
28861
  ].forEach((opt) => cmd.addOption(opt));
28862
+ cmd.addOption(deprecatedRegionOption);
28800
28863
  return cmd;
28801
28864
  }
28802
28865
 
@@ -28865,6 +28928,7 @@ async function diffCommand(stacks, options) {
28865
28928
  if (options.verbose) {
28866
28929
  logger.setLevel("debug");
28867
28930
  }
28931
+ warnIfDeprecatedRegion(options);
28868
28932
  const app = resolveApp(options.app);
28869
28933
  if (!app) {
28870
28934
  throw new Error(
@@ -29008,19 +29072,219 @@ function createDiffCommand() {
29008
29072
  [...commonOptions, ...appOptions, ...stateOptions, ...stackOptions, ...contextOptions].forEach(
29009
29073
  (opt) => cmd.addOption(opt)
29010
29074
  );
29075
+ cmd.addOption(deprecatedRegionOption);
29011
29076
  return cmd;
29012
29077
  }
29013
29078
 
29014
29079
  // src/cli/commands/destroy.ts
29015
29080
  import { Command as Command6 } from "commander";
29016
29081
  init_aws_clients();
29082
+
29083
+ // src/cli/commands/destroy-runner.ts
29017
29084
  import * as readline from "node:readline/promises";
29085
+ init_aws_clients();
29086
+ async function runDestroyForStack(stackName, state, ctx) {
29087
+ const logger = getLogger();
29088
+ const result = {
29089
+ stackName,
29090
+ cancelled: false,
29091
+ skippedEmpty: false,
29092
+ deletedCount: 0,
29093
+ errorCount: 0
29094
+ };
29095
+ const resourceCount = Object.keys(state.resources).length;
29096
+ const regionForState = state.region ?? ctx.baseRegion;
29097
+ if (resourceCount === 0) {
29098
+ logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
29099
+ await ctx.stateBackend.deleteState(stackName, regionForState);
29100
+ logger.info("\u2713 State deleted");
29101
+ result.skippedEmpty = true;
29102
+ return result;
29103
+ }
29104
+ logger.info(`
29105
+ Resources to be deleted (${resourceCount}):`);
29106
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
29107
+ logger.info(` - ${logicalId} (${resource.resourceType})`);
29108
+ }
29109
+ if (!ctx.skipConfirmation) {
29110
+ const rl = readline.createInterface({
29111
+ input: process.stdin,
29112
+ output: process.stdout
29113
+ });
29114
+ const answer = await rl.question(
29115
+ `
29116
+ Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
29117
+ );
29118
+ rl.close();
29119
+ const trimmed = answer.trim().toLowerCase();
29120
+ if (trimmed === "n" || trimmed === "no") {
29121
+ logger.info("Destroy cancelled");
29122
+ result.cancelled = true;
29123
+ return result;
29124
+ }
29125
+ }
29126
+ const stackRegion = state.region;
29127
+ let destroyProviderRegistry = ctx.providerRegistry;
29128
+ let destroyAwsClients;
29129
+ if (stackRegion && stackRegion !== ctx.baseRegion) {
29130
+ logger.info(`Stack region: ${stackRegion}`);
29131
+ process.env["AWS_REGION"] = stackRegion;
29132
+ process.env["AWS_DEFAULT_REGION"] = stackRegion;
29133
+ destroyAwsClients = new AwsClients({
29134
+ region: stackRegion,
29135
+ ...ctx.profile && { profile: ctx.profile }
29136
+ });
29137
+ setAwsClients(destroyAwsClients);
29138
+ destroyProviderRegistry = new ProviderRegistry();
29139
+ registerAllProviders(destroyProviderRegistry);
29140
+ destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
29141
+ }
29142
+ logger.info(`
29143
+ Acquiring lock for stack ${stackName}...`);
29144
+ await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
29145
+ const renderer = getLiveRenderer();
29146
+ renderer.start();
29147
+ try {
29148
+ logger.info("Building dependency graph...");
29149
+ const template = {
29150
+ AWSTemplateFormatVersion: "2010-09-09",
29151
+ Resources: {}
29152
+ };
29153
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
29154
+ template.Resources[logicalId] = {
29155
+ Type: resource.resourceType,
29156
+ Properties: resource.properties || {},
29157
+ ...resource.dependencies && resource.dependencies.length > 0 && {
29158
+ DependsOn: resource.dependencies
29159
+ }
29160
+ };
29161
+ }
29162
+ const typeToLogicalIds = /* @__PURE__ */ new Map();
29163
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
29164
+ const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
29165
+ ids.push(logicalId);
29166
+ typeToLogicalIds.set(resource.resourceType, ids);
29167
+ }
29168
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
29169
+ const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
29170
+ if (!mustDeleteAfter)
29171
+ continue;
29172
+ for (const depType of mustDeleteAfter) {
29173
+ const depIds = typeToLogicalIds.get(depType);
29174
+ if (!depIds)
29175
+ continue;
29176
+ for (const depId of depIds) {
29177
+ const existing = template.Resources[depId]?.DependsOn ?? [];
29178
+ const depsArray = Array.isArray(existing) ? existing : [existing];
29179
+ if (!depsArray.includes(logicalId)) {
29180
+ template.Resources[depId] = {
29181
+ ...template.Resources[depId],
29182
+ DependsOn: [...depsArray, logicalId]
29183
+ };
29184
+ logger.debug(
29185
+ `Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
29186
+ );
29187
+ }
29188
+ }
29189
+ }
29190
+ }
29191
+ const dagBuilder = new DagBuilder();
29192
+ const graph = dagBuilder.buildGraph(template);
29193
+ const executionLevels = dagBuilder.getExecutionLevels(graph);
29194
+ logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
29195
+ for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
29196
+ const level = executionLevels[levelIndex];
29197
+ if (!level)
29198
+ continue;
29199
+ logger.debug(
29200
+ `Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
29201
+ );
29202
+ const deletePromises = level.map(async (logicalId) => {
29203
+ const resource = state.resources[logicalId];
29204
+ if (!resource) {
29205
+ logger.warn(`Resource ${logicalId} not found in state, skipping`);
29206
+ return;
29207
+ }
29208
+ renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
29209
+ try {
29210
+ const provider = destroyProviderRegistry.getProvider(resource.resourceType);
29211
+ let lastDeleteError;
29212
+ for (let attempt = 0; attempt <= 3; attempt++) {
29213
+ try {
29214
+ await provider.delete(
29215
+ logicalId,
29216
+ resource.physicalId,
29217
+ resource.resourceType,
29218
+ resource.properties
29219
+ );
29220
+ lastDeleteError = null;
29221
+ break;
29222
+ } catch (retryError) {
29223
+ lastDeleteError = retryError;
29224
+ const msg = retryError instanceof Error ? retryError.message : String(retryError);
29225
+ const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
29226
+ if (!isRetryable || attempt >= 3)
29227
+ break;
29228
+ const delay = 5e3 * Math.pow(2, attempt);
29229
+ logger.debug(
29230
+ ` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
29231
+ );
29232
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
29233
+ }
29234
+ }
29235
+ if (lastDeleteError)
29236
+ throw lastDeleteError;
29237
+ renderer.removeTask(logicalId);
29238
+ logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
29239
+ result.deletedCount++;
29240
+ } catch (error) {
29241
+ renderer.removeTask(logicalId);
29242
+ const msg = error instanceof Error ? error.message : String(error);
29243
+ if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
29244
+ logger.debug(` ${logicalId} already deleted, removing from state`);
29245
+ result.deletedCount++;
29246
+ } else {
29247
+ logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
29248
+ result.errorCount++;
29249
+ }
29250
+ } finally {
29251
+ renderer.removeTask(logicalId);
29252
+ }
29253
+ });
29254
+ await Promise.all(deletePromises);
29255
+ }
29256
+ if (result.errorCount === 0) {
29257
+ await ctx.stateBackend.deleteState(stackName, regionForState);
29258
+ logger.debug("State deleted");
29259
+ } else {
29260
+ logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
29261
+ }
29262
+ logger.info(
29263
+ `
29264
+ \u2713 Stack ${stackName} destroyed (${result.deletedCount} deleted, ${result.errorCount} errors)`
29265
+ );
29266
+ } finally {
29267
+ renderer.stop();
29268
+ logger.debug("Releasing lock...");
29269
+ await ctx.lockManager.releaseLock(stackName, regionForState);
29270
+ if (destroyAwsClients) {
29271
+ destroyAwsClients.destroy();
29272
+ process.env["AWS_REGION"] = ctx.baseRegion;
29273
+ process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
29274
+ setAwsClients(ctx.baseAwsClients);
29275
+ }
29276
+ }
29277
+ return result;
29278
+ }
29279
+
29280
+ // src/cli/commands/destroy.ts
29018
29281
  async function destroyCommand(stackArgs, options) {
29019
29282
  const logger = getLogger();
29020
29283
  if (options.verbose) {
29021
29284
  logger.setLevel("debug");
29022
29285
  process.env["CDKD_NO_LIVE"] = "1";
29023
29286
  }
29287
+ warnIfDeprecatedRegion(options);
29024
29288
  const region = options.region || process.env["AWS_REGION"] || "us-east-1";
29025
29289
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
29026
29290
  logger.info("Starting stack destruction...");
@@ -29045,7 +29309,6 @@ async function destroyCommand(stackArgs, options) {
29045
29309
  });
29046
29310
  await stateBackend.verifyBucketExists();
29047
29311
  const lockManager = new LockManager(awsClients.s3, stateConfig);
29048
- const dagBuilder = new DagBuilder();
29049
29312
  const providerRegistry = new ProviderRegistry();
29050
29313
  registerAllProviders(providerRegistry);
29051
29314
  providerRegistry.setCustomResourceResponseBucket(stateBucket);
@@ -29145,189 +29408,16 @@ Preparing to destroy stack: ${stackName}`);
29145
29408
  logger.warn(`No state found for stack ${stackName}, skipping`);
29146
29409
  continue;
29147
29410
  }
29148
- const currentState = stateResult.state;
29149
- const resourceCount = Object.keys(currentState.resources).length;
29150
- if (resourceCount === 0) {
29151
- logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
29152
- await stateBackend.deleteState(stackName, stackTargetRegion);
29153
- logger.info("\u2713 State deleted");
29154
- continue;
29155
- }
29156
- logger.info(`
29157
- Resources to be deleted (${resourceCount}):`);
29158
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29159
- logger.info(` - ${logicalId} (${resource.resourceType})`);
29160
- }
29161
- if (!options.yes && !options.force) {
29162
- const rl = readline.createInterface({
29163
- input: process.stdin,
29164
- output: process.stdout
29165
- });
29166
- const answer = await rl.question(
29167
- `
29168
- Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
29169
- );
29170
- rl.close();
29171
- const trimmed = answer.trim().toLowerCase();
29172
- if (trimmed === "n" || trimmed === "no") {
29173
- logger.info("Destroy cancelled");
29174
- continue;
29175
- }
29176
- }
29177
- const stackRegion = stackTargetRegion;
29178
- let destroyProviderRegistry = providerRegistry;
29179
- let destroyAwsClients;
29180
- if (stackRegion && stackRegion !== region) {
29181
- logger.info(`Stack region: ${stackRegion}`);
29182
- process.env["AWS_REGION"] = stackRegion;
29183
- process.env["AWS_DEFAULT_REGION"] = stackRegion;
29184
- destroyAwsClients = new AwsClients({
29185
- region: stackRegion,
29186
- ...options.profile && { profile: options.profile }
29187
- });
29188
- setAwsClients(destroyAwsClients);
29189
- destroyProviderRegistry = new ProviderRegistry();
29190
- registerAllProviders(destroyProviderRegistry);
29191
- destroyProviderRegistry.setCustomResourceResponseBucket(stateBucket);
29192
- }
29193
- logger.info(`
29194
- Acquiring lock for stack ${stackName}...`);
29195
- await lockManager.acquireLock(stackName, stackRegion, void 0, "destroy");
29196
- const renderer = getLiveRenderer();
29197
- renderer.start();
29198
- try {
29199
- logger.info("Building dependency graph...");
29200
- const template = {
29201
- AWSTemplateFormatVersion: "2010-09-09",
29202
- Resources: {}
29203
- };
29204
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29205
- template.Resources[logicalId] = {
29206
- Type: resource.resourceType,
29207
- Properties: resource.properties || {},
29208
- ...resource.dependencies && resource.dependencies.length > 0 && {
29209
- DependsOn: resource.dependencies
29210
- }
29211
- };
29212
- }
29213
- const typeToLogicalIds = /* @__PURE__ */ new Map();
29214
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29215
- const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
29216
- ids.push(logicalId);
29217
- typeToLogicalIds.set(resource.resourceType, ids);
29218
- }
29219
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29220
- const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
29221
- if (!mustDeleteAfter)
29222
- continue;
29223
- for (const depType of mustDeleteAfter) {
29224
- const depIds = typeToLogicalIds.get(depType);
29225
- if (!depIds)
29226
- continue;
29227
- for (const depId of depIds) {
29228
- const existing = template.Resources[depId]?.DependsOn ?? [];
29229
- const depsArray = Array.isArray(existing) ? existing : [existing];
29230
- if (!depsArray.includes(logicalId)) {
29231
- template.Resources[depId] = {
29232
- ...template.Resources[depId],
29233
- DependsOn: [...depsArray, logicalId]
29234
- };
29235
- logger.debug(
29236
- `Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
29237
- );
29238
- }
29239
- }
29240
- }
29241
- }
29242
- const graph = dagBuilder.buildGraph(template);
29243
- const executionLevels = dagBuilder.getExecutionLevels(graph);
29244
- logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
29245
- let deletedCount = 0;
29246
- let errorCount = 0;
29247
- for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
29248
- const level = executionLevels[levelIndex];
29249
- if (!level) {
29250
- continue;
29251
- }
29252
- logger.debug(
29253
- `Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
29254
- );
29255
- const deletePromises = level.map(async (logicalId) => {
29256
- const resource = currentState.resources[logicalId];
29257
- if (!resource) {
29258
- logger.warn(`Resource ${logicalId} not found in state, skipping`);
29259
- return;
29260
- }
29261
- renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
29262
- try {
29263
- const provider = destroyProviderRegistry.getProvider(resource.resourceType);
29264
- let lastDeleteError;
29265
- for (let attempt = 0; attempt <= 3; attempt++) {
29266
- try {
29267
- await provider.delete(
29268
- logicalId,
29269
- resource.physicalId,
29270
- resource.resourceType,
29271
- resource.properties,
29272
- { expectedRegion: currentState.region }
29273
- );
29274
- lastDeleteError = null;
29275
- break;
29276
- } catch (retryError) {
29277
- lastDeleteError = retryError;
29278
- const msg = retryError instanceof Error ? retryError.message : String(retryError);
29279
- const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
29280
- if (!isRetryable || attempt >= 3)
29281
- break;
29282
- const delay = 5e3 * Math.pow(2, attempt);
29283
- logger.debug(
29284
- ` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
29285
- );
29286
- await new Promise((resolve4) => setTimeout(resolve4, delay));
29287
- }
29288
- }
29289
- if (lastDeleteError)
29290
- throw lastDeleteError;
29291
- renderer.removeTask(logicalId);
29292
- logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
29293
- deletedCount++;
29294
- } catch (error) {
29295
- renderer.removeTask(logicalId);
29296
- const msg = error instanceof Error ? error.message : String(error);
29297
- if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
29298
- logger.debug(` ${logicalId} already deleted, removing from state`);
29299
- deletedCount++;
29300
- } else {
29301
- logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
29302
- errorCount++;
29303
- }
29304
- } finally {
29305
- renderer.removeTask(logicalId);
29306
- }
29307
- });
29308
- await Promise.all(deletePromises);
29309
- }
29310
- if (errorCount === 0) {
29311
- await stateBackend.deleteState(stackName, stackRegion);
29312
- logger.debug("State deleted");
29313
- } else {
29314
- logger.warn(`${errorCount} resource(s) failed to delete. State preserved.`);
29315
- }
29316
- logger.info(
29317
- `
29318
- \u2713 Stack ${stackName} destroyed (${deletedCount} deleted, ${errorCount} errors)`
29319
- );
29320
- } finally {
29321
- renderer.stop();
29322
- logger.debug("Releasing lock...");
29323
- await lockManager.releaseLock(stackName, stackRegion);
29324
- if (destroyAwsClients) {
29325
- destroyAwsClients.destroy();
29326
- process.env["AWS_REGION"] = region;
29327
- process.env["AWS_DEFAULT_REGION"] = region;
29328
- setAwsClients(awsClients);
29329
- }
29330
- }
29411
+ await runDestroyForStack(stackName, stateResult.state, {
29412
+ stateBackend,
29413
+ lockManager,
29414
+ providerRegistry,
29415
+ baseAwsClients: awsClients,
29416
+ baseRegion: region,
29417
+ ...options.profile && { profile: options.profile },
29418
+ stateBucket,
29419
+ skipConfirmation: options.yes || options.force
29420
+ });
29331
29421
  }
29332
29422
  } finally {
29333
29423
  awsClients.destroy();
@@ -29346,16 +29436,18 @@ function createDestroyCommand() {
29346
29436
  ...destroyOptions,
29347
29437
  ...contextOptions
29348
29438
  ].forEach((opt) => cmd.addOption(opt));
29439
+ cmd.addOption(deprecatedRegionOption);
29349
29440
  return cmd;
29350
29441
  }
29351
29442
 
29352
29443
  // src/cli/commands/publish-assets.ts
29353
- import { Option as Option2, Command as Command7 } from "commander";
29444
+ import { Option as Option3, Command as Command7 } from "commander";
29354
29445
  async function publishAssetsCommand(options) {
29355
29446
  const logger = getLogger();
29356
29447
  if (options.verbose) {
29357
29448
  logger.setLevel("debug");
29358
29449
  }
29450
+ warnIfDeprecatedRegion(options);
29359
29451
  logger.info("Publishing assets...");
29360
29452
  logger.debug("Asset manifest path:", options.path);
29361
29453
  const publisher = new AssetPublisher();
@@ -29369,25 +29461,27 @@ async function publishAssetsCommand(options) {
29369
29461
  }
29370
29462
  function createPublishAssetsCommand() {
29371
29463
  const cmd = new Command7("publish-assets").description("Publish assets to S3/ECR from asset manifest").requiredOption("--path <path>", "Path to asset manifest file or directory").addOption(
29372
- new Option2(
29464
+ new Option3(
29373
29465
  "--asset-publish-concurrency <number>",
29374
29466
  "Maximum concurrent asset publish operations"
29375
29467
  ).default(8).argParser((value) => parseInt(value, 10))
29376
29468
  ).addOption(
29377
- new Option2("--image-build-concurrency <number>", "Maximum concurrent Docker image builds").default(4).argParser((value) => parseInt(value, 10))
29469
+ new Option3("--image-build-concurrency <number>", "Maximum concurrent Docker image builds").default(4).argParser((value) => parseInt(value, 10))
29378
29470
  ).action(withErrorHandling(publishAssetsCommand));
29379
29471
  commonOptions.forEach((opt) => cmd.addOption(opt));
29472
+ cmd.addOption(deprecatedRegionOption);
29380
29473
  return cmd;
29381
29474
  }
29382
29475
 
29383
29476
  // src/cli/commands/force-unlock.ts
29384
- import { Command as Command8, Option as Option3 } from "commander";
29477
+ import { Command as Command8, Option as Option4 } from "commander";
29385
29478
  init_aws_clients();
29386
29479
  async function forceUnlockCommand(stackArgs, options) {
29387
29480
  const logger = getLogger();
29388
29481
  if (options.verbose) {
29389
29482
  logger.setLevel("debug");
29390
29483
  }
29484
+ warnIfDeprecatedRegion(options);
29391
29485
  const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
29392
29486
  if (stackPatterns.length === 0) {
29393
29487
  throw new Error("Stack name is required. Usage: cdkd force-unlock <stack-name>");
@@ -29441,18 +29535,19 @@ async function forceUnlockCommand(stackArgs, options) {
29441
29535
  }
29442
29536
  function createForceUnlockCommand() {
29443
29537
  const cmd = new Command8("force-unlock").description("Force-release a stale lock on a stack").argument("[stacks...]", "Stack name(s) to unlock").addOption(
29444
- new Option3(
29538
+ new Option4(
29445
29539
  "--stack-region <region>",
29446
29540
  "Stack region whose lock to release (use when the same stack name has locks in multiple regions). Defaults to all regions where the stack has state."
29447
29541
  )
29448
29542
  ).action(withErrorHandling(forceUnlockCommand));
29449
29543
  [...commonOptions, ...stateOptions, ...stackOptions].forEach((opt) => cmd.addOption(opt));
29544
+ cmd.addOption(deprecatedRegionOption);
29450
29545
  return cmd;
29451
29546
  }
29452
29547
 
29453
29548
  // src/cli/commands/state.ts
29454
29549
  import * as readline2 from "node:readline/promises";
29455
- import { Command as Command9, Option as Option4 } from "commander";
29550
+ import { Command as Command9, Option as Option5 } from "commander";
29456
29551
  init_aws_clients();
29457
29552
  function formatStackRef(ref) {
29458
29553
  return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
@@ -29482,6 +29577,7 @@ function resolveSingleRegion(stackName, refs, requestedRegion) {
29482
29577
  );
29483
29578
  }
29484
29579
  async function setupStateBackend(options) {
29580
+ warnIfDeprecatedRegion(options);
29485
29581
  const awsClients = new AwsClients({
29486
29582
  ...options.region && { region: options.region },
29487
29583
  ...options.profile && { profile: options.profile }
@@ -29500,6 +29596,8 @@ async function setupStateBackend(options) {
29500
29596
  return {
29501
29597
  stateBackend,
29502
29598
  lockManager,
29599
+ awsClients,
29600
+ region,
29503
29601
  bucket,
29504
29602
  prefix,
29505
29603
  dispose: () => awsClients.destroy()
@@ -29590,6 +29688,7 @@ async function stateListCommand(options) {
29590
29688
  function createStateListCommand() {
29591
29689
  const cmd = new Command9("list").alias("ls").description("List stacks registered in the cdkd state bucket").option("-l, --long", "Show resource count, last-modified time, and lock status", false).option("--json", "Output as JSON", false).action(withErrorHandling(stateListCommand));
29592
29690
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29691
+ cmd.addOption(deprecatedRegionOption);
29593
29692
  return cmd;
29594
29693
  }
29595
29694
  async function stateResourcesCommand(stackName, options) {
@@ -29693,6 +29792,7 @@ function formatLockSummary(lockInfo) {
29693
29792
  function createStateResourcesCommand() {
29694
29793
  const cmd = new Command9("resources").description("List resources recorded in a stack's state").argument("<stack>", "Stack name (physical CloudFormation name)").option("-l, --long", "Include dependencies and attributes per resource", false).option("--json", "Output as JSON", false).addOption(stackRegionOption()).action(withErrorHandling(stateResourcesCommand));
29695
29794
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29795
+ cmd.addOption(deprecatedRegionOption);
29696
29796
  return cmd;
29697
29797
  }
29698
29798
  async function stateShowCommand(stackName, options) {
@@ -29780,6 +29880,7 @@ async function stateShowCommand(stackName, options) {
29780
29880
  function createStateShowCommand() {
29781
29881
  const cmd = new Command9("show").description("Show the full cdkd state record for a stack (metadata, outputs, resources)").argument("<stack>", "Stack name (physical CloudFormation name)").option("--json", "Output the raw state and lock as JSON", false).addOption(stackRegionOption()).action(withErrorHandling(stateShowCommand));
29782
29882
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29883
+ cmd.addOption(deprecatedRegionOption);
29783
29884
  return cmd;
29784
29885
  }
29785
29886
  async function stateRmCommand(stackArgs, options) {
@@ -29854,7 +29955,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
29854
29955
  }
29855
29956
  }
29856
29957
  function stackRegionOption() {
29857
- return new Option4(
29958
+ return new Option5(
29858
29959
  "--stack-region <region>",
29859
29960
  "Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
29860
29961
  );
@@ -29862,6 +29963,147 @@ function stackRegionOption() {
29862
29963
  function createStateRmCommand() {
29863
29964
  const cmd = new Command9("rm").description("Remove cdkd state for one or more stacks (does NOT delete AWS resources)").argument("<stacks...>", "Stack name(s) to remove from state").option("-f, --force", "Skip confirmation and remove even if the stack is locked", false).addOption(stackRegionOption()).action(withErrorHandling(stateRmCommand));
29864
29965
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29966
+ cmd.addOption(deprecatedRegionOption);
29967
+ return cmd;
29968
+ }
29969
+ async function stateDestroyCommand(stackArgs, options) {
29970
+ const logger = getLogger();
29971
+ if (options.verbose) {
29972
+ logger.setLevel("debug");
29973
+ process.env["CDKD_NO_LIVE"] = "1";
29974
+ }
29975
+ if (!options.all && stackArgs.length === 0) {
29976
+ throw new Error(
29977
+ "Stack name is required. Usage: cdkd state destroy <stack> [<stack>...] | --all"
29978
+ );
29979
+ }
29980
+ const setup = await setupStateBackend(options);
29981
+ const providerRegistry = new ProviderRegistry();
29982
+ registerAllProviders(providerRegistry);
29983
+ providerRegistry.setCustomResourceResponseBucket(setup.bucket);
29984
+ try {
29985
+ const stateRefs = await setup.stateBackend.listStacks();
29986
+ const knownStackNames = new Set(stateRefs.map((r) => r.stackName));
29987
+ let stackNames;
29988
+ if (options.all) {
29989
+ stackNames = [...knownStackNames].sort();
29990
+ if (stackNames.length === 0) {
29991
+ logger.info("No stacks found in state");
29992
+ return;
29993
+ }
29994
+ } else {
29995
+ const missing = stackArgs.filter((name) => !knownStackNames.has(name));
29996
+ if (missing.length > 0) {
29997
+ throw new Error(
29998
+ `No state found for stack(s): ${missing.join(", ")}. Run 'cdkd state list' to see available stacks.`
29999
+ );
30000
+ }
30001
+ stackNames = stackArgs;
30002
+ }
30003
+ if (options.all && !options.yes) {
30004
+ process.stdout.write(
30005
+ `
30006
+ WARNING: This destroys ${stackNames.length} stack(s) and removes their state records:
30007
+ `
30008
+ );
30009
+ for (const name of stackNames) {
30010
+ process.stdout.write(` - ${name}
30011
+ `);
30012
+ }
30013
+ process.stdout.write("\n");
30014
+ const rl = readline2.createInterface({
30015
+ input: process.stdin,
30016
+ output: process.stdout
30017
+ });
30018
+ const answer = await rl.question(`Destroy all ${stackNames.length} stack(s)? (y/N): `);
30019
+ rl.close();
30020
+ const trimmed = answer.trim().toLowerCase();
30021
+ if (trimmed !== "y" && trimmed !== "yes") {
30022
+ logger.info("Destroy cancelled");
30023
+ return;
30024
+ }
30025
+ }
30026
+ logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
30027
+ let totalErrors = 0;
30028
+ for (const stackName of stackNames) {
30029
+ const refs = stateRefs.filter((r) => r.stackName === stackName);
30030
+ let targets;
30031
+ if (options.stackRegion) {
30032
+ targets = refs.filter((r) => r.region === options.stackRegion || !r.region);
30033
+ if (targets.length === 0) {
30034
+ logger.warn(
30035
+ `Skipping ${stackName}: no state record matches --stack-region '${options.stackRegion}'`
30036
+ );
30037
+ continue;
30038
+ }
30039
+ } else if (refs.length === 1) {
30040
+ targets = refs;
30041
+ } else {
30042
+ const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
30043
+ throw new Error(
30044
+ `Stack '${stackName}' has state in multiple regions: ${regions}. Use --region <region> to pick one.`
30045
+ );
30046
+ }
30047
+ for (const ref of targets) {
30048
+ logger.info(
30049
+ `
30050
+ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
30051
+ );
30052
+ const stateResult = await setup.stateBackend.getState(
30053
+ stackName,
30054
+ ref.region ?? setup.region
30055
+ );
30056
+ if (!stateResult) {
30057
+ logger.warn(
30058
+ `No state found for stack ${stackName}${ref.region ? ` in ${ref.region}` : ""}, skipping`
30059
+ );
30060
+ continue;
30061
+ }
30062
+ const result = await runDestroyForStack(stackName, stateResult.state, {
30063
+ stateBackend: setup.stateBackend,
30064
+ lockManager: setup.lockManager,
30065
+ providerRegistry,
30066
+ baseAwsClients: setup.awsClients,
30067
+ baseRegion: setup.region,
30068
+ ...options.profile && { profile: options.profile },
30069
+ stateBucket: setup.bucket,
30070
+ // --yes covers both the --all batch prompt above (already consumed)
30071
+ // and the per-stack prompt inside the runner. Per-stack prompts are
30072
+ // skipped when `options.yes` is set OR `--all` was set (the user
30073
+ // already accepted the batch prompt).
30074
+ skipConfirmation: options.yes || options.all === true
30075
+ });
30076
+ totalErrors += result.errorCount;
30077
+ }
30078
+ }
30079
+ if (totalErrors > 0) {
30080
+ throw new Error(
30081
+ `Destroy completed with ${totalErrors} resource error(s). Inspect 'cdkd state show <stack>' and re-run.`
30082
+ );
30083
+ }
30084
+ } finally {
30085
+ setup.dispose();
30086
+ }
30087
+ }
30088
+ function createStateDestroyCommand() {
30089
+ const cmd = new Command9("destroy").description(
30090
+ "Destroy a stack's AWS resources and remove its state record without requiring the CDK app. For removing only the state record (keeping AWS resources intact), use 'cdkd state rm'."
30091
+ ).argument("[stacks...]", "Stack name(s) to destroy (physical CloudFormation names)").option("--all", "Destroy every stack in the state bucket", false).addOption(stackRegionOption()).addHelpText(
30092
+ "after",
30093
+ [
30094
+ "",
30095
+ "Examples:",
30096
+ " cdkd state destroy MyStack",
30097
+ " cdkd state destroy MyStack OtherStack",
30098
+ " cdkd state destroy --all -y",
30099
+ " cdkd state destroy MyStack --state-bucket cdkd-state-test",
30100
+ " cdkd state destroy MyStack --stack-region us-west-2",
30101
+ "",
30102
+ "For removing only the state record (keeping AWS resources intact), use 'cdkd state rm'."
30103
+ ].join("\n")
30104
+ ).action(withErrorHandling(stateDestroyCommand));
30105
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
30106
+ cmd.addOption(deprecatedRegionOption);
29865
30107
  return cmd;
29866
30108
  }
29867
30109
  function createStateCommand() {
@@ -29870,6 +30112,7 @@ function createStateCommand() {
29870
30112
  cmd.addCommand(createStateResourcesCommand());
29871
30113
  cmd.addCommand(createStateShowCommand());
29872
30114
  cmd.addCommand(createStateRmCommand());
30115
+ cmd.addCommand(createStateDestroyCommand());
29873
30116
  return cmd;
29874
30117
  }
29875
30118
 
@@ -29898,7 +30141,7 @@ function reorderArgs(argv) {
29898
30141
  }
29899
30142
  async function main() {
29900
30143
  const program = new Command10();
29901
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.10.0");
30144
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.12.0");
29902
30145
  program.addCommand(createBootstrapCommand());
29903
30146
  program.addCommand(createSynthCommand());
29904
30147
  program.addCommand(createListCommand());