@go-to-k/cdkd 0.11.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>",
@@ -1158,7 +1168,16 @@ function createBootstrapCommand() {
1158
1168
  const cmd = new Command("bootstrap").description("Bootstrap cdkd by creating required S3 bucket for state management").option(
1159
1169
  "--state-bucket <bucket>",
1160
1170
  "Name of S3 bucket to create for state storage (default: cdkd-state-{accountId})"
1161
- ).option("--force", "Force reconfiguration of existing bucket", false).action(withErrorHandling(bootstrapCommand));
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));
1162
1181
  commonOptions.forEach((opt) => cmd.addOption(opt));
1163
1182
  return cmd;
1164
1183
  }
@@ -2493,6 +2512,7 @@ async function synthCommand(options) {
2493
2512
  if (options.verbose) {
2494
2513
  logger.setLevel("debug");
2495
2514
  }
2515
+ warnIfDeprecatedRegion(options);
2496
2516
  const app = resolveApp(options.app);
2497
2517
  if (!app) {
2498
2518
  throw new Error(
@@ -2540,6 +2560,7 @@ Output: ${assemblyDir}`);
2540
2560
  function createSynthCommand() {
2541
2561
  const cmd = new Command2("synth").description("Synthesize CDK app to CloudFormation template").action(withErrorHandling(synthCommand));
2542
2562
  [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
2563
+ cmd.addOption(deprecatedRegionOption);
2543
2564
  return cmd;
2544
2565
  }
2545
2566
 
@@ -2622,6 +2643,7 @@ async function listCommand(patterns, options) {
2622
2643
  if (options.verbose) {
2623
2644
  logger.setLevel("debug");
2624
2645
  }
2646
+ warnIfDeprecatedRegion(options);
2625
2647
  const app = resolveApp(options.app);
2626
2648
  if (!app) {
2627
2649
  throw new Error(
@@ -2687,6 +2709,7 @@ function createListCommand() {
2687
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/*')."
2688
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));
2689
2711
  [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
2712
+ cmd.addOption(deprecatedRegionOption);
2690
2713
  return cmd;
2691
2714
  }
2692
2715
 
@@ -28586,6 +28609,7 @@ async function deployCommand(stacks, options) {
28586
28609
  logger.setLevel("debug");
28587
28610
  process.env["CDKD_NO_LIVE"] = "1";
28588
28611
  }
28612
+ warnIfDeprecatedRegion(options);
28589
28613
  if (!options.wait) {
28590
28614
  process.env["CDKD_NO_WAIT"] = "true";
28591
28615
  }
@@ -28835,6 +28859,7 @@ function createDeployCommand() {
28835
28859
  ...deployOptions,
28836
28860
  ...contextOptions
28837
28861
  ].forEach((opt) => cmd.addOption(opt));
28862
+ cmd.addOption(deprecatedRegionOption);
28838
28863
  return cmd;
28839
28864
  }
28840
28865
 
@@ -28903,6 +28928,7 @@ async function diffCommand(stacks, options) {
28903
28928
  if (options.verbose) {
28904
28929
  logger.setLevel("debug");
28905
28930
  }
28931
+ warnIfDeprecatedRegion(options);
28906
28932
  const app = resolveApp(options.app);
28907
28933
  if (!app) {
28908
28934
  throw new Error(
@@ -29046,19 +29072,219 @@ function createDiffCommand() {
29046
29072
  [...commonOptions, ...appOptions, ...stateOptions, ...stackOptions, ...contextOptions].forEach(
29047
29073
  (opt) => cmd.addOption(opt)
29048
29074
  );
29075
+ cmd.addOption(deprecatedRegionOption);
29049
29076
  return cmd;
29050
29077
  }
29051
29078
 
29052
29079
  // src/cli/commands/destroy.ts
29053
29080
  import { Command as Command6 } from "commander";
29054
29081
  init_aws_clients();
29082
+
29083
+ // src/cli/commands/destroy-runner.ts
29055
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
29056
29281
  async function destroyCommand(stackArgs, options) {
29057
29282
  const logger = getLogger();
29058
29283
  if (options.verbose) {
29059
29284
  logger.setLevel("debug");
29060
29285
  process.env["CDKD_NO_LIVE"] = "1";
29061
29286
  }
29287
+ warnIfDeprecatedRegion(options);
29062
29288
  const region = options.region || process.env["AWS_REGION"] || "us-east-1";
29063
29289
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
29064
29290
  logger.info("Starting stack destruction...");
@@ -29083,7 +29309,6 @@ async function destroyCommand(stackArgs, options) {
29083
29309
  });
29084
29310
  await stateBackend.verifyBucketExists();
29085
29311
  const lockManager = new LockManager(awsClients.s3, stateConfig);
29086
- const dagBuilder = new DagBuilder();
29087
29312
  const providerRegistry = new ProviderRegistry();
29088
29313
  registerAllProviders(providerRegistry);
29089
29314
  providerRegistry.setCustomResourceResponseBucket(stateBucket);
@@ -29183,189 +29408,16 @@ Preparing to destroy stack: ${stackName}`);
29183
29408
  logger.warn(`No state found for stack ${stackName}, skipping`);
29184
29409
  continue;
29185
29410
  }
29186
- const currentState = stateResult.state;
29187
- const resourceCount = Object.keys(currentState.resources).length;
29188
- if (resourceCount === 0) {
29189
- logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
29190
- await stateBackend.deleteState(stackName, stackTargetRegion);
29191
- logger.info("\u2713 State deleted");
29192
- continue;
29193
- }
29194
- logger.info(`
29195
- Resources to be deleted (${resourceCount}):`);
29196
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29197
- logger.info(` - ${logicalId} (${resource.resourceType})`);
29198
- }
29199
- if (!options.yes && !options.force) {
29200
- const rl = readline.createInterface({
29201
- input: process.stdin,
29202
- output: process.stdout
29203
- });
29204
- const answer = await rl.question(
29205
- `
29206
- Are you sure you want to destroy stack "${stackName}" and delete all ${resourceCount} resources? (Y/n): `
29207
- );
29208
- rl.close();
29209
- const trimmed = answer.trim().toLowerCase();
29210
- if (trimmed === "n" || trimmed === "no") {
29211
- logger.info("Destroy cancelled");
29212
- continue;
29213
- }
29214
- }
29215
- const stackRegion = stackTargetRegion;
29216
- let destroyProviderRegistry = providerRegistry;
29217
- let destroyAwsClients;
29218
- if (stackRegion && stackRegion !== region) {
29219
- logger.info(`Stack region: ${stackRegion}`);
29220
- process.env["AWS_REGION"] = stackRegion;
29221
- process.env["AWS_DEFAULT_REGION"] = stackRegion;
29222
- destroyAwsClients = new AwsClients({
29223
- region: stackRegion,
29224
- ...options.profile && { profile: options.profile }
29225
- });
29226
- setAwsClients(destroyAwsClients);
29227
- destroyProviderRegistry = new ProviderRegistry();
29228
- registerAllProviders(destroyProviderRegistry);
29229
- destroyProviderRegistry.setCustomResourceResponseBucket(stateBucket);
29230
- }
29231
- logger.info(`
29232
- Acquiring lock for stack ${stackName}...`);
29233
- await lockManager.acquireLock(stackName, stackRegion, void 0, "destroy");
29234
- const renderer = getLiveRenderer();
29235
- renderer.start();
29236
- try {
29237
- logger.info("Building dependency graph...");
29238
- const template = {
29239
- AWSTemplateFormatVersion: "2010-09-09",
29240
- Resources: {}
29241
- };
29242
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29243
- template.Resources[logicalId] = {
29244
- Type: resource.resourceType,
29245
- Properties: resource.properties || {},
29246
- ...resource.dependencies && resource.dependencies.length > 0 && {
29247
- DependsOn: resource.dependencies
29248
- }
29249
- };
29250
- }
29251
- const typeToLogicalIds = /* @__PURE__ */ new Map();
29252
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29253
- const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
29254
- ids.push(logicalId);
29255
- typeToLogicalIds.set(resource.resourceType, ids);
29256
- }
29257
- for (const [logicalId, resource] of Object.entries(currentState.resources)) {
29258
- const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
29259
- if (!mustDeleteAfter)
29260
- continue;
29261
- for (const depType of mustDeleteAfter) {
29262
- const depIds = typeToLogicalIds.get(depType);
29263
- if (!depIds)
29264
- continue;
29265
- for (const depId of depIds) {
29266
- const existing = template.Resources[depId]?.DependsOn ?? [];
29267
- const depsArray = Array.isArray(existing) ? existing : [existing];
29268
- if (!depsArray.includes(logicalId)) {
29269
- template.Resources[depId] = {
29270
- ...template.Resources[depId],
29271
- DependsOn: [...depsArray, logicalId]
29272
- };
29273
- logger.debug(
29274
- `Implicit delete dependency: ${depId} (${depType}) must be deleted before ${logicalId} (${resource.resourceType})`
29275
- );
29276
- }
29277
- }
29278
- }
29279
- }
29280
- const graph = dagBuilder.buildGraph(template);
29281
- const executionLevels = dagBuilder.getExecutionLevels(graph);
29282
- logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
29283
- let deletedCount = 0;
29284
- let errorCount = 0;
29285
- for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
29286
- const level = executionLevels[levelIndex];
29287
- if (!level) {
29288
- continue;
29289
- }
29290
- logger.debug(
29291
- `Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
29292
- );
29293
- const deletePromises = level.map(async (logicalId) => {
29294
- const resource = currentState.resources[logicalId];
29295
- if (!resource) {
29296
- logger.warn(`Resource ${logicalId} not found in state, skipping`);
29297
- return;
29298
- }
29299
- renderer.addTask(logicalId, `Deleting ${logicalId} (${resource.resourceType})`);
29300
- try {
29301
- const provider = destroyProviderRegistry.getProvider(resource.resourceType);
29302
- let lastDeleteError;
29303
- for (let attempt = 0; attempt <= 3; attempt++) {
29304
- try {
29305
- await provider.delete(
29306
- logicalId,
29307
- resource.physicalId,
29308
- resource.resourceType,
29309
- resource.properties,
29310
- { expectedRegion: currentState.region }
29311
- );
29312
- lastDeleteError = null;
29313
- break;
29314
- } catch (retryError) {
29315
- lastDeleteError = retryError;
29316
- const msg = retryError instanceof Error ? retryError.message : String(retryError);
29317
- const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
29318
- if (!isRetryable || attempt >= 3)
29319
- break;
29320
- const delay = 5e3 * Math.pow(2, attempt);
29321
- logger.debug(
29322
- ` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
29323
- );
29324
- await new Promise((resolve4) => setTimeout(resolve4, delay));
29325
- }
29326
- }
29327
- if (lastDeleteError)
29328
- throw lastDeleteError;
29329
- renderer.removeTask(logicalId);
29330
- logger.info(` \u2705 ${logicalId} (${resource.resourceType}) deleted`);
29331
- deletedCount++;
29332
- } catch (error) {
29333
- renderer.removeTask(logicalId);
29334
- const msg = error instanceof Error ? error.message : String(error);
29335
- if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
29336
- logger.debug(` ${logicalId} already deleted, removing from state`);
29337
- deletedCount++;
29338
- } else {
29339
- logger.error(` \u2717 Failed to delete ${logicalId}:`, String(error));
29340
- errorCount++;
29341
- }
29342
- } finally {
29343
- renderer.removeTask(logicalId);
29344
- }
29345
- });
29346
- await Promise.all(deletePromises);
29347
- }
29348
- if (errorCount === 0) {
29349
- await stateBackend.deleteState(stackName, stackRegion);
29350
- logger.debug("State deleted");
29351
- } else {
29352
- logger.warn(`${errorCount} resource(s) failed to delete. State preserved.`);
29353
- }
29354
- logger.info(
29355
- `
29356
- \u2713 Stack ${stackName} destroyed (${deletedCount} deleted, ${errorCount} errors)`
29357
- );
29358
- } finally {
29359
- renderer.stop();
29360
- logger.debug("Releasing lock...");
29361
- await lockManager.releaseLock(stackName, stackRegion);
29362
- if (destroyAwsClients) {
29363
- destroyAwsClients.destroy();
29364
- process.env["AWS_REGION"] = region;
29365
- process.env["AWS_DEFAULT_REGION"] = region;
29366
- setAwsClients(awsClients);
29367
- }
29368
- }
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
+ });
29369
29421
  }
29370
29422
  } finally {
29371
29423
  awsClients.destroy();
@@ -29384,16 +29436,18 @@ function createDestroyCommand() {
29384
29436
  ...destroyOptions,
29385
29437
  ...contextOptions
29386
29438
  ].forEach((opt) => cmd.addOption(opt));
29439
+ cmd.addOption(deprecatedRegionOption);
29387
29440
  return cmd;
29388
29441
  }
29389
29442
 
29390
29443
  // src/cli/commands/publish-assets.ts
29391
- import { Option as Option2, Command as Command7 } from "commander";
29444
+ import { Option as Option3, Command as Command7 } from "commander";
29392
29445
  async function publishAssetsCommand(options) {
29393
29446
  const logger = getLogger();
29394
29447
  if (options.verbose) {
29395
29448
  logger.setLevel("debug");
29396
29449
  }
29450
+ warnIfDeprecatedRegion(options);
29397
29451
  logger.info("Publishing assets...");
29398
29452
  logger.debug("Asset manifest path:", options.path);
29399
29453
  const publisher = new AssetPublisher();
@@ -29407,25 +29461,27 @@ async function publishAssetsCommand(options) {
29407
29461
  }
29408
29462
  function createPublishAssetsCommand() {
29409
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(
29410
- new Option2(
29464
+ new Option3(
29411
29465
  "--asset-publish-concurrency <number>",
29412
29466
  "Maximum concurrent asset publish operations"
29413
29467
  ).default(8).argParser((value) => parseInt(value, 10))
29414
29468
  ).addOption(
29415
- 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))
29416
29470
  ).action(withErrorHandling(publishAssetsCommand));
29417
29471
  commonOptions.forEach((opt) => cmd.addOption(opt));
29472
+ cmd.addOption(deprecatedRegionOption);
29418
29473
  return cmd;
29419
29474
  }
29420
29475
 
29421
29476
  // src/cli/commands/force-unlock.ts
29422
- import { Command as Command8, Option as Option3 } from "commander";
29477
+ import { Command as Command8, Option as Option4 } from "commander";
29423
29478
  init_aws_clients();
29424
29479
  async function forceUnlockCommand(stackArgs, options) {
29425
29480
  const logger = getLogger();
29426
29481
  if (options.verbose) {
29427
29482
  logger.setLevel("debug");
29428
29483
  }
29484
+ warnIfDeprecatedRegion(options);
29429
29485
  const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
29430
29486
  if (stackPatterns.length === 0) {
29431
29487
  throw new Error("Stack name is required. Usage: cdkd force-unlock <stack-name>");
@@ -29479,18 +29535,19 @@ async function forceUnlockCommand(stackArgs, options) {
29479
29535
  }
29480
29536
  function createForceUnlockCommand() {
29481
29537
  const cmd = new Command8("force-unlock").description("Force-release a stale lock on a stack").argument("[stacks...]", "Stack name(s) to unlock").addOption(
29482
- new Option3(
29538
+ new Option4(
29483
29539
  "--stack-region <region>",
29484
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."
29485
29541
  )
29486
29542
  ).action(withErrorHandling(forceUnlockCommand));
29487
29543
  [...commonOptions, ...stateOptions, ...stackOptions].forEach((opt) => cmd.addOption(opt));
29544
+ cmd.addOption(deprecatedRegionOption);
29488
29545
  return cmd;
29489
29546
  }
29490
29547
 
29491
29548
  // src/cli/commands/state.ts
29492
29549
  import * as readline2 from "node:readline/promises";
29493
- import { Command as Command9, Option as Option4 } from "commander";
29550
+ import { Command as Command9, Option as Option5 } from "commander";
29494
29551
  init_aws_clients();
29495
29552
  function formatStackRef(ref) {
29496
29553
  return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
@@ -29520,6 +29577,7 @@ function resolveSingleRegion(stackName, refs, requestedRegion) {
29520
29577
  );
29521
29578
  }
29522
29579
  async function setupStateBackend(options) {
29580
+ warnIfDeprecatedRegion(options);
29523
29581
  const awsClients = new AwsClients({
29524
29582
  ...options.region && { region: options.region },
29525
29583
  ...options.profile && { profile: options.profile }
@@ -29538,6 +29596,8 @@ async function setupStateBackend(options) {
29538
29596
  return {
29539
29597
  stateBackend,
29540
29598
  lockManager,
29599
+ awsClients,
29600
+ region,
29541
29601
  bucket,
29542
29602
  prefix,
29543
29603
  dispose: () => awsClients.destroy()
@@ -29628,6 +29688,7 @@ async function stateListCommand(options) {
29628
29688
  function createStateListCommand() {
29629
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));
29630
29690
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29691
+ cmd.addOption(deprecatedRegionOption);
29631
29692
  return cmd;
29632
29693
  }
29633
29694
  async function stateResourcesCommand(stackName, options) {
@@ -29731,6 +29792,7 @@ function formatLockSummary(lockInfo) {
29731
29792
  function createStateResourcesCommand() {
29732
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));
29733
29794
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29795
+ cmd.addOption(deprecatedRegionOption);
29734
29796
  return cmd;
29735
29797
  }
29736
29798
  async function stateShowCommand(stackName, options) {
@@ -29818,6 +29880,7 @@ async function stateShowCommand(stackName, options) {
29818
29880
  function createStateShowCommand() {
29819
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));
29820
29882
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
29883
+ cmd.addOption(deprecatedRegionOption);
29821
29884
  return cmd;
29822
29885
  }
29823
29886
  async function stateRmCommand(stackArgs, options) {
@@ -29892,7 +29955,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
29892
29955
  }
29893
29956
  }
29894
29957
  function stackRegionOption() {
29895
- return new Option4(
29958
+ return new Option5(
29896
29959
  "--stack-region <region>",
29897
29960
  "Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
29898
29961
  );
@@ -29900,6 +29963,147 @@ function stackRegionOption() {
29900
29963
  function createStateRmCommand() {
29901
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));
29902
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);
29903
30107
  return cmd;
29904
30108
  }
29905
30109
  function createStateCommand() {
@@ -29908,6 +30112,7 @@ function createStateCommand() {
29908
30112
  cmd.addCommand(createStateResourcesCommand());
29909
30113
  cmd.addCommand(createStateShowCommand());
29910
30114
  cmd.addCommand(createStateRmCommand());
30115
+ cmd.addCommand(createStateDestroyCommand());
29911
30116
  return cmd;
29912
30117
  }
29913
30118
 
@@ -29936,7 +30141,7 @@ function reorderArgs(argv) {
29936
30141
  }
29937
30142
  async function main() {
29938
30143
  const program = new Command10();
29939
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.11.0");
30144
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.12.0");
29940
30145
  program.addCommand(createBootstrapCommand());
29941
30146
  program.addCommand(createSynthCommand());
29942
30147
  program.addCommand(createListCommand());