@go-to-k/cdkd 0.7.0 → 0.8.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
@@ -3205,6 +3205,14 @@ import {
3205
3205
  ListObjectsV2Command,
3206
3206
  NoSuchKey
3207
3207
  } from "@aws-sdk/client-s3";
3208
+
3209
+ // src/types/state.ts
3210
+ var STATE_SCHEMA_VERSION_LEGACY = 1;
3211
+ var STATE_SCHEMA_VERSION_CURRENT = 2;
3212
+
3213
+ // src/state/s3-state-backend.ts
3214
+ var LEGACY_KEY_DEPTH = 2;
3215
+ var NEW_KEY_DEPTH = 3;
3208
3216
  var S3StateBackend = class {
3209
3217
  constructor(s3Client, config) {
3210
3218
  this.s3Client = s3Client;
@@ -3212,9 +3220,16 @@ var S3StateBackend = class {
3212
3220
  }
3213
3221
  logger = getLogger().child("S3StateBackend");
3214
3222
  /**
3215
- * Get the S3 key for a stack's state file
3223
+ * Get the new (region-scoped) S3 key for a stack's state file.
3224
+ */
3225
+ getStateKey(stackName, region) {
3226
+ return `${this.config.prefix}/${stackName}/${region}/state.json`;
3227
+ }
3228
+ /**
3229
+ * Get the legacy (pre-region-prefix) S3 key for a stack's state file.
3230
+ * Used for backwards-compatible reads and for the migration delete.
3216
3231
  */
3217
- getStateKey(stackName) {
3232
+ getLegacyStateKey(stackName) {
3218
3233
  return `${this.config.prefix}/${stackName}/state.json`;
3219
3234
  }
3220
3235
  /**
@@ -3240,101 +3255,143 @@ var S3StateBackend = class {
3240
3255
  }
3241
3256
  }
3242
3257
  /**
3243
- * Check if state exists for a stack
3244
- */
3245
- async stateExists(stackName) {
3246
- const key = this.getStateKey(stackName);
3247
- try {
3248
- await this.s3Client.send(
3249
- new HeadObjectCommand2({
3250
- Bucket: this.config.bucket,
3251
- Key: key
3252
- })
3253
- );
3258
+ * Check if state exists for a stack in the given region.
3259
+ *
3260
+ * Returns true for either layout: the new region-scoped key, or the legacy
3261
+ * key when its embedded `region` matches the requested region. This lets
3262
+ * `cdkd state rm <stack> --region X` and `cdkd destroy <stack>` see legacy
3263
+ * state without forcing a write-through migration first.
3264
+ */
3265
+ async stateExists(stackName, region) {
3266
+ const newKey = this.getStateKey(stackName, region);
3267
+ if (await this.headObject(newKey)) {
3254
3268
  return true;
3255
- } catch (error) {
3256
- if (error instanceof NoSuchKey || error.name === "NotFound") {
3257
- return false;
3258
- }
3259
- throw new StateError(
3260
- `Failed to check if state exists for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3261
- error instanceof Error ? error : void 0
3262
- );
3263
3269
  }
3270
+ return this.legacyMatchesRegion(stackName, region);
3264
3271
  }
3265
3272
  /**
3266
- * Get state for a stack
3273
+ * Get state for a stack, transparently falling back to the legacy key.
3274
+ *
3275
+ * Lookup order:
3276
+ * 1. `{prefix}/{stackName}/{region}/state.json` (current `version: 2` key).
3277
+ * 2. `{prefix}/{stackName}/state.json` (legacy `version: 1` key) — only
3278
+ * accepted if its embedded `region` matches the requested region.
3267
3279
  *
3268
- * Note: S3 returns ETag with surrounding quotes (e.g., "abc123").
3269
- * We preserve the quotes as they are required for IfMatch conditions.
3280
+ * When a legacy hit is returned, `migrationPending` is `true`. Callers that
3281
+ * subsequently `saveState` automatically migrate by writing the new key and
3282
+ * deleting the legacy one (see `saveState`'s `legacyMigration` argument).
3283
+ *
3284
+ * Note: S3 returns ETag with surrounding quotes (e.g., `"abc123"`). We
3285
+ * preserve the quotes — they are required for `IfMatch` conditions.
3270
3286
  */
3271
- async getState(stackName) {
3272
- const key = this.getStateKey(stackName);
3287
+ async getState(stackName, region) {
3288
+ const newKey = this.getStateKey(stackName, region);
3273
3289
  try {
3274
- this.logger.debug(`Getting state for stack: ${stackName}`);
3290
+ this.logger.debug(`Getting state for stack: ${stackName} (${region})`);
3275
3291
  const response = await this.s3Client.send(
3276
3292
  new GetObjectCommand({
3277
3293
  Bucket: this.config.bucket,
3278
- Key: key
3294
+ Key: newKey
3279
3295
  })
3280
3296
  );
3281
3297
  if (!response.Body) {
3282
- throw new StateError(`State file for stack '${stackName}' has no body`);
3298
+ throw new StateError(`State file for stack '${stackName}' (${region}) has no body`);
3283
3299
  }
3284
3300
  if (!response.ETag) {
3285
- throw new StateError(`State file for stack '${stackName}' has no ETag`);
3301
+ throw new StateError(`State file for stack '${stackName}' (${region}) has no ETag`);
3286
3302
  }
3287
3303
  const bodyString = await response.Body.transformToString();
3288
- const state = JSON.parse(bodyString);
3289
- this.logger.debug(`Retrieved state for stack: ${stackName}, ETag: ${response.ETag}`);
3290
- return {
3291
- state,
3292
- etag: response.ETag
3293
- };
3304
+ const state = this.parseStateBody(bodyString, stackName);
3305
+ this.logger.debug(`Retrieved state: ${stackName} (${region}), ETag: ${response.ETag}`);
3306
+ return { state, etag: response.ETag };
3294
3307
  } catch (error) {
3295
- if (error instanceof NoSuchKey || error.name === "NoSuchKey") {
3296
- this.logger.debug(`No existing state for stack: ${stackName}`);
3297
- return null;
3298
- }
3299
- if (error instanceof StateError) {
3300
- throw error;
3308
+ if (!isNoSuchKey(error)) {
3309
+ if (error instanceof StateError)
3310
+ throw error;
3311
+ throw new StateError(
3312
+ `Failed to get state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3313
+ error instanceof Error ? error : void 0
3314
+ );
3301
3315
  }
3302
- throw new StateError(
3303
- `Failed to get state for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3304
- error instanceof Error ? error : void 0
3316
+ this.logger.debug(`No state at new key for stack: ${stackName} (${region})`);
3317
+ }
3318
+ const legacy = await this.tryGetLegacy(stackName, region);
3319
+ if (legacy) {
3320
+ this.logger.warn(
3321
+ `Loaded legacy state for stack '${stackName}' from '${this.getLegacyStateKey(stackName)}'. It will be migrated to the region-scoped layout on next save.`
3305
3322
  );
3323
+ return { ...legacy, migrationPending: true };
3306
3324
  }
3325
+ return null;
3307
3326
  }
3308
3327
  /**
3309
- * Save state for a stack with optimistic locking
3328
+ * Save state for a stack with optimistic locking.
3329
+ *
3330
+ * Always writes to the new region-scoped key. The state body is rewritten
3331
+ * with `version: 2` and the supplied region.
3332
+ *
3333
+ * If the caller observed `migrationPending: true` from `getState`, it
3334
+ * should pass the legacy ETag back via `expectedEtag` AND set
3335
+ * `migrateLegacy: true`. After the new key is written successfully, the
3336
+ * legacy key is deleted to complete migration. The legacy delete is a
3337
+ * best-effort follow-up — a failure is logged but does not unwind the new
3338
+ * write.
3310
3339
  *
3311
3340
  * @param stackName Stack name
3341
+ * @param region Target region (load-bearing — part of the S3 key)
3312
3342
  * @param state State to save
3313
- * @param expectedEtag Expected ETag for optimistic locking (optional for new state).
3314
- * Must include quotes if provided (e.g., "abc123")
3315
- * @returns New ETag (with quotes, e.g., "abc123")
3316
- */
3317
- async saveState(stackName, state, expectedEtag) {
3318
- const key = this.getStateKey(stackName);
3343
+ * @param options Optimistic-lock ETag + legacy-migration flag
3344
+ * @returns New ETag (with quotes, e.g., `"abc123"`)
3345
+ */
3346
+ async saveState(stackName, region, state, options = {}) {
3347
+ const newKey = this.getStateKey(stackName, region);
3348
+ const { expectedEtag, migrateLegacy } = options;
3349
+ const body = {
3350
+ ...state,
3351
+ version: STATE_SCHEMA_VERSION_CURRENT,
3352
+ stackName,
3353
+ region
3354
+ };
3319
3355
  try {
3320
3356
  this.logger.debug(
3321
- `Saving state for stack: ${stackName}${expectedEtag ? `, expected ETag: ${expectedEtag}` : ""}`
3357
+ `Saving state: ${stackName} (${region})${expectedEtag ? `, expected ETag: ${expectedEtag}` : ""}`
3322
3358
  );
3323
- const body = JSON.stringify(state, null, 2);
3359
+ const bodyString = JSON.stringify(body, null, 2);
3324
3360
  const response = await this.s3Client.send(
3325
3361
  new PutObjectCommand2({
3326
3362
  Bucket: this.config.bucket,
3327
- Key: key,
3328
- Body: body,
3329
- ContentLength: Buffer.byteLength(body),
3363
+ Key: newKey,
3364
+ Body: bodyString,
3365
+ ContentLength: Buffer.byteLength(bodyString),
3330
3366
  ContentType: "application/json",
3331
- ...expectedEtag && { IfMatch: expectedEtag }
3367
+ // The legacy ETag is for a different key; only forward it when we're
3368
+ // updating in-place at the new key.
3369
+ ...!migrateLegacy && expectedEtag && { IfMatch: expectedEtag }
3332
3370
  })
3333
3371
  );
3334
3372
  if (!response.ETag) {
3335
- throw new StateError(`No ETag returned after saving state for stack '${stackName}'`);
3373
+ throw new StateError(
3374
+ `No ETag returned after saving state for stack '${stackName}' (${region})`
3375
+ );
3376
+ }
3377
+ this.logger.debug(`State saved: ${stackName} (${region}), new ETag: ${response.ETag}`);
3378
+ if (migrateLegacy) {
3379
+ try {
3380
+ await this.s3Client.send(
3381
+ new DeleteObjectCommand({
3382
+ Bucket: this.config.bucket,
3383
+ Key: this.getLegacyStateKey(stackName)
3384
+ })
3385
+ );
3386
+ this.logger.info(
3387
+ `Migrated state for stack '${stackName}' to region-scoped layout (${region})`
3388
+ );
3389
+ } catch (deleteError) {
3390
+ this.logger.warn(
3391
+ `Migrated stack '${stackName}' to new key, but failed to delete legacy key: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
3392
+ );
3393
+ }
3336
3394
  }
3337
- this.logger.debug(`State saved for stack: ${stackName}, new ETag: ${response.ETag}`);
3338
3395
  return response.ETag;
3339
3396
  } catch (error) {
3340
3397
  if (error.name === "PreconditionFailed") {
@@ -3343,63 +3400,230 @@ var S3StateBackend = class {
3343
3400
  );
3344
3401
  }
3345
3402
  throw new StateError(
3346
- `Failed to save state for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3403
+ `Failed to save state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3347
3404
  error instanceof Error ? error : void 0
3348
3405
  );
3349
3406
  }
3350
3407
  }
3351
3408
  /**
3352
- * Delete state for a stack
3409
+ * Delete state for a stack in the given region.
3410
+ *
3411
+ * Removes both the new key and the legacy key (if present). Legacy removal
3412
+ * is region-conditional: a legacy state file with a different `region`
3413
+ * field is left alone.
3353
3414
  */
3354
- async deleteState(stackName) {
3355
- const key = this.getStateKey(stackName);
3415
+ async deleteState(stackName, region) {
3356
3416
  try {
3357
- this.logger.debug(`Deleting state for stack: ${stackName}`);
3417
+ this.logger.debug(`Deleting state: ${stackName} (${region})`);
3358
3418
  await this.s3Client.send(
3359
3419
  new DeleteObjectCommand({
3360
3420
  Bucket: this.config.bucket,
3361
- Key: key
3421
+ Key: this.getStateKey(stackName, region)
3362
3422
  })
3363
3423
  );
3364
- this.logger.debug(`State deleted for stack: ${stackName}`);
3424
+ if (await this.legacyMatchesRegion(stackName, region)) {
3425
+ await this.s3Client.send(
3426
+ new DeleteObjectCommand({
3427
+ Bucket: this.config.bucket,
3428
+ Key: this.getLegacyStateKey(stackName)
3429
+ })
3430
+ );
3431
+ this.logger.debug(`Deleted legacy state for stack: ${stackName}`);
3432
+ }
3433
+ this.logger.debug(`State deleted: ${stackName} (${region})`);
3365
3434
  } catch (error) {
3366
3435
  throw new StateError(
3367
- `Failed to delete state for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3436
+ `Failed to delete state for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3368
3437
  error instanceof Error ? error : void 0
3369
3438
  );
3370
3439
  }
3371
3440
  }
3372
3441
  /**
3373
- * List all stacks with state
3442
+ * List all stacks with state in the bucket.
3443
+ *
3444
+ * Returns one `{stackName, region}` pair per state file. Both layouts
3445
+ * are enumerated:
3446
+ *
3447
+ * - `{prefix}/{stackName}/{region}/state.json` (new) — `region` is the
3448
+ * path segment.
3449
+ * - `{prefix}/{stackName}/state.json` (legacy) — `region` is read from the
3450
+ * state body when present, otherwise `undefined`.
3451
+ *
3452
+ * Pairs are deduplicated by `(stackName, region)` so a stack mid-migration
3453
+ * shows up exactly once.
3374
3454
  */
3375
3455
  async listStacks() {
3376
3456
  try {
3377
3457
  this.logger.debug("Listing all stacks");
3458
+ const prefix = `${this.config.prefix}/`;
3459
+ const refs = [];
3460
+ const seen = /* @__PURE__ */ new Set();
3461
+ let continuationToken;
3462
+ do {
3463
+ const response = await this.s3Client.send(
3464
+ new ListObjectsV2Command({
3465
+ Bucket: this.config.bucket,
3466
+ Prefix: prefix,
3467
+ ...continuationToken && { ContinuationToken: continuationToken }
3468
+ })
3469
+ );
3470
+ for (const obj of response.Contents ?? []) {
3471
+ const key = obj.Key;
3472
+ if (!key)
3473
+ continue;
3474
+ if (!key.endsWith("/state.json"))
3475
+ continue;
3476
+ const rest = key.slice(prefix.length);
3477
+ const segments = rest.split("/");
3478
+ if (segments.length === NEW_KEY_DEPTH) {
3479
+ const [stackName, region] = segments;
3480
+ if (!stackName || !region)
3481
+ continue;
3482
+ const dedupeKey = `${stackName}\0${region}`;
3483
+ if (!seen.has(dedupeKey)) {
3484
+ seen.add(dedupeKey);
3485
+ refs.push({ stackName, region });
3486
+ }
3487
+ continue;
3488
+ }
3489
+ if (segments.length === LEGACY_KEY_DEPTH) {
3490
+ const [stackName] = segments;
3491
+ if (!stackName)
3492
+ continue;
3493
+ const region = await this.readLegacyRegion(stackName);
3494
+ const dedupeKey = `${stackName}\0${region ?? ""}`;
3495
+ if (!seen.has(dedupeKey)) {
3496
+ seen.add(dedupeKey);
3497
+ refs.push({ stackName, ...region ? { region } : {} });
3498
+ }
3499
+ }
3500
+ }
3501
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
3502
+ } while (continuationToken);
3503
+ this.logger.debug(`Found ${refs.length} stack(s) across regions`);
3504
+ return refs;
3505
+ } catch (error) {
3506
+ throw new StateError(
3507
+ `Failed to list stacks: ${error instanceof Error ? error.message : String(error)}`,
3508
+ error instanceof Error ? error : void 0
3509
+ );
3510
+ }
3511
+ }
3512
+ /**
3513
+ * HeadObject probe — returns true on 200, false on NotFound. Other errors
3514
+ * propagate so we don't accidentally swallow IAM denials.
3515
+ */
3516
+ async headObject(key) {
3517
+ try {
3518
+ await this.s3Client.send(
3519
+ new HeadObjectCommand2({
3520
+ Bucket: this.config.bucket,
3521
+ Key: key
3522
+ })
3523
+ );
3524
+ return true;
3525
+ } catch (error) {
3526
+ if (isNoSuchKey(error) || error.name === "NotFound") {
3527
+ return false;
3528
+ }
3529
+ throw error;
3530
+ }
3531
+ }
3532
+ /**
3533
+ * Read the legacy state's `region` field. Used for region matching during
3534
+ * `stateExists` / `deleteState` and for assigning a region to legacy
3535
+ * entries during `listStacks`.
3536
+ */
3537
+ async readLegacyRegion(stackName) {
3538
+ try {
3539
+ const response = await this.s3Client.send(
3540
+ new GetObjectCommand({
3541
+ Bucket: this.config.bucket,
3542
+ Key: this.getLegacyStateKey(stackName)
3543
+ })
3544
+ );
3545
+ if (!response.Body)
3546
+ return void 0;
3547
+ const bodyString = await response.Body.transformToString();
3548
+ const state = JSON.parse(bodyString);
3549
+ return typeof state.region === "string" ? state.region : void 0;
3550
+ } catch (error) {
3551
+ if (isNoSuchKey(error))
3552
+ return void 0;
3553
+ this.logger.debug(
3554
+ `Could not read legacy state region for '${stackName}': ${error instanceof Error ? error.message : String(error)}`
3555
+ );
3556
+ return void 0;
3557
+ }
3558
+ }
3559
+ async legacyMatchesRegion(stackName, region) {
3560
+ const legacyRegion = await this.readLegacyRegion(stackName);
3561
+ return legacyRegion === region;
3562
+ }
3563
+ /**
3564
+ * Try to read the legacy `version: 1` state. Returns null when the legacy
3565
+ * key is missing or its embedded region does not match the caller's region.
3566
+ */
3567
+ async tryGetLegacy(stackName, region) {
3568
+ try {
3378
3569
  const response = await this.s3Client.send(
3379
- new ListObjectsV2Command({
3570
+ new GetObjectCommand({
3380
3571
  Bucket: this.config.bucket,
3381
- Prefix: `${this.config.prefix}/`,
3382
- Delimiter: "/"
3572
+ Key: this.getLegacyStateKey(stackName)
3383
3573
  })
3384
3574
  );
3385
- if (!response.CommonPrefixes) {
3386
- return [];
3575
+ if (!response.Body || !response.ETag) {
3576
+ return null;
3577
+ }
3578
+ const bodyString = await response.Body.transformToString();
3579
+ const state = this.parseStateBody(bodyString, stackName);
3580
+ if (state.region && state.region !== region) {
3581
+ this.logger.debug(
3582
+ `Legacy state for stack '${stackName}' has region '${state.region}', not '${region}' \u2014 skipping legacy fallback.`
3583
+ );
3584
+ return null;
3387
3585
  }
3388
- const stackNames = response.CommonPrefixes.map((prefix) => {
3389
- const prefixStr = prefix.Prefix || "";
3390
- const parts = prefixStr.split("/");
3391
- return parts[parts.length - 2];
3392
- }).filter((name) => Boolean(name));
3393
- this.logger.debug(`Found ${stackNames.length} stacks`);
3394
- return stackNames;
3586
+ return { state, etag: response.ETag };
3395
3587
  } catch (error) {
3588
+ if (isNoSuchKey(error))
3589
+ return null;
3396
3590
  throw new StateError(
3397
- `Failed to list stacks: ${error instanceof Error ? error.message : String(error)}`,
3591
+ `Failed to get legacy state for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3592
+ error instanceof Error ? error : void 0
3593
+ );
3594
+ }
3595
+ }
3596
+ /**
3597
+ * Parse a state body and validate the schema version. Future-proofs against
3598
+ * a binary that predates schema version `N` reading a `version: N+1` blob:
3599
+ * the old binary would otherwise treat unknown fields as defaults and
3600
+ * silently lose data on the next save.
3601
+ */
3602
+ parseStateBody(bodyString, stackName) {
3603
+ let parsed;
3604
+ try {
3605
+ parsed = JSON.parse(bodyString);
3606
+ } catch (error) {
3607
+ throw new StateError(
3608
+ `State file for stack '${stackName}' is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
3398
3609
  error instanceof Error ? error : void 0
3399
3610
  );
3400
3611
  }
3612
+ const v = parsed.version;
3613
+ if (v !== STATE_SCHEMA_VERSION_LEGACY && v !== STATE_SCHEMA_VERSION_CURRENT && v !== void 0) {
3614
+ throw new StateError(
3615
+ `Unsupported state schema version ${String(v)} for stack '${stackName}'. This cdkd binary supports versions ${String(STATE_SCHEMA_VERSION_LEGACY)} and ${String(STATE_SCHEMA_VERSION_CURRENT)}. Upgrade cdkd to a version that supports schema ${String(v)}.`
3616
+ );
3617
+ }
3618
+ return parsed;
3401
3619
  }
3402
3620
  };
3621
+ function isNoSuchKey(error) {
3622
+ if (error instanceof NoSuchKey)
3623
+ return true;
3624
+ const name = error?.name;
3625
+ return name === "NoSuchKey";
3626
+ }
3403
3627
 
3404
3628
  // src/state/lock-manager.ts
3405
3629
  import {
@@ -3420,10 +3644,24 @@ var LockManager = class {
3420
3644
  logger = getLogger().child("LockManager");
3421
3645
  ttlMs;
3422
3646
  /**
3423
- * Get the S3 key for a stack's lock file
3647
+ * Get the S3 key for a stack's lock file.
3648
+ *
3649
+ * Locks are region-scoped, mirroring the state key layout
3650
+ * (`{prefix}/{stackName}/{region}/lock.json`). Two regions of the same
3651
+ * stackName can therefore be operated on in parallel without contention,
3652
+ * matching cdkd's parallel execution model.
3653
+ *
3654
+ * The `region` argument is required for new callers; for backwards
3655
+ * compatibility with `state list --long` (which only sees stack names),
3656
+ * passing `undefined` falls back to the legacy `{prefix}/{stackName}/lock.json`
3657
+ * key — that mode is purely for legacy lock cleanup and is NOT used by
3658
+ * deploy / destroy / diff anymore.
3424
3659
  */
3425
- getLockKey(stackName) {
3426
- return `${this.config.prefix}/${stackName}/lock.json`;
3660
+ getLockKey(stackName, region) {
3661
+ if (region === void 0) {
3662
+ return `${this.config.prefix}/${stackName}/lock.json`;
3663
+ }
3664
+ return `${this.config.prefix}/${stackName}/${region}/lock.json`;
3427
3665
  }
3428
3666
  /**
3429
3667
  * Get default lock owner identifier
@@ -3462,11 +3700,12 @@ var LockManager = class {
3462
3700
  * If an expired lock exists, it will be cleaned up and re-acquired.
3463
3701
  *
3464
3702
  * @param stackName Stack name
3703
+ * @param region Target region (lock key is region-scoped)
3465
3704
  * @param owner Lock owner identifier (defaults to user@hostname:pid)
3466
3705
  * @param operation Operation being performed (e.g., "deploy", "destroy")
3467
3706
  */
3468
- async acquireLock(stackName, owner, operation) {
3469
- const key = this.getLockKey(stackName);
3707
+ async acquireLock(stackName, region, owner, operation) {
3708
+ const key = this.getLockKey(stackName, region);
3470
3709
  const lockOwner = owner || this.getDefaultOwner();
3471
3710
  const now = Date.now();
3472
3711
  const lockInfo = {
@@ -3476,7 +3715,7 @@ var LockManager = class {
3476
3715
  ...operation && { operation }
3477
3716
  };
3478
3717
  try {
3479
- this.logger.debug(`Attempting to acquire lock for stack: ${stackName}`);
3718
+ this.logger.debug(`Attempting to acquire lock for stack: ${stackName} (${region})`);
3480
3719
  const lockBody = JSON.stringify(lockInfo, null, 2);
3481
3720
  await this.s3Client.send(
3482
3721
  new PutObjectCommand3({
@@ -3489,17 +3728,17 @@ var LockManager = class {
3489
3728
  // Only succeed if object doesn't exist
3490
3729
  })
3491
3730
  );
3492
- this.logger.debug(`Lock acquired for stack: ${stackName}, owner: ${lockOwner}`);
3731
+ this.logger.debug(`Lock acquired for stack: ${stackName} (${region}), owner: ${lockOwner}`);
3493
3732
  return true;
3494
3733
  } catch (error) {
3495
3734
  if (error instanceof S3ServiceException && error.name === "PreconditionFailed") {
3496
- this.logger.debug(`Lock already exists for stack: ${stackName}`);
3497
- const existingLock = await this.getLockInfo(stackName);
3735
+ this.logger.debug(`Lock already exists for stack: ${stackName} (${region})`);
3736
+ const existingLock = await this.getLockInfo(stackName, region);
3498
3737
  if (existingLock && this.isLockExpired(existingLock)) {
3499
3738
  this.logger.info(
3500
- `Expired lock detected for stack: ${stackName} (owner: ${existingLock.owner}, expired ${this.formatDuration(now - existingLock.expiresAt)} ago). Cleaning up...`
3739
+ `Expired lock detected for stack: ${stackName} (${region}, owner: ${existingLock.owner}, expired ${this.formatDuration(now - existingLock.expiresAt)} ago). Cleaning up...`
3501
3740
  );
3502
- await this.deleteLock(stackName);
3741
+ await this.deleteLock(stackName, region);
3503
3742
  try {
3504
3743
  const retryBody = JSON.stringify(lockInfo, null, 2);
3505
3744
  await this.s3Client.send(
@@ -3513,13 +3752,13 @@ var LockManager = class {
3513
3752
  })
3514
3753
  );
3515
3754
  this.logger.debug(
3516
- `Lock acquired for stack: ${stackName} after expired lock cleanup, owner: ${lockOwner}`
3755
+ `Lock acquired for stack: ${stackName} (${region}) after expired lock cleanup, owner: ${lockOwner}`
3517
3756
  );
3518
3757
  return true;
3519
3758
  } catch (retryError) {
3520
3759
  if (retryError instanceof S3ServiceException && retryError.name === "PreconditionFailed") {
3521
3760
  this.logger.debug(
3522
- `Lock was acquired by another process during expired lock cleanup for stack: ${stackName}`
3761
+ `Lock was acquired by another process during expired lock cleanup for stack: ${stackName} (${region})`
3523
3762
  );
3524
3763
  return false;
3525
3764
  }
@@ -3529,16 +3768,20 @@ var LockManager = class {
3529
3768
  return false;
3530
3769
  }
3531
3770
  throw new LockError(
3532
- `Failed to acquire lock for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3771
+ `Failed to acquire lock for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3533
3772
  error instanceof Error ? error : void 0
3534
3773
  );
3535
3774
  }
3536
3775
  }
3537
3776
  /**
3538
- * Get current lock information
3777
+ * Get current lock information.
3778
+ *
3779
+ * `region` is required for the new region-scoped lock layout. Pass
3780
+ * `undefined` only to inspect a legacy `{prefix}/{stackName}/lock.json`
3781
+ * file (e.g. for state-listing tools that don't yet know the region).
3539
3782
  */
3540
- async getLockInfo(stackName) {
3541
- const key = this.getLockKey(stackName);
3783
+ async getLockInfo(stackName, region) {
3784
+ const key = this.getLockKey(stackName, region);
3542
3785
  try {
3543
3786
  this.logger.debug(`Getting lock info for stack: ${stackName}`);
3544
3787
  const response = await this.s3Client.send(
@@ -3576,27 +3819,27 @@ var LockManager = class {
3576
3819
  * not for acquisition decisions — use `acquireLock` for that, which has its
3577
3820
  * own expired-lock cleanup logic.
3578
3821
  */
3579
- async isLocked(stackName) {
3580
- const lockInfo = await this.getLockInfo(stackName);
3822
+ async isLocked(stackName, region) {
3823
+ const lockInfo = await this.getLockInfo(stackName, region);
3581
3824
  return lockInfo !== null;
3582
3825
  }
3583
3826
  /**
3584
3827
  * Release a lock for a stack
3585
3828
  */
3586
- async releaseLock(stackName) {
3587
- const key = this.getLockKey(stackName);
3829
+ async releaseLock(stackName, region) {
3830
+ const key = this.getLockKey(stackName, region);
3588
3831
  try {
3589
- this.logger.debug(`Releasing lock for stack: ${stackName}`);
3832
+ this.logger.debug(`Releasing lock for stack: ${stackName} (${region})`);
3590
3833
  await this.s3Client.send(
3591
3834
  new DeleteObjectCommand2({
3592
3835
  Bucket: this.config.bucket,
3593
3836
  Key: key
3594
3837
  })
3595
3838
  );
3596
- this.logger.debug(`Lock released for stack: ${stackName}`);
3839
+ this.logger.debug(`Lock released for stack: ${stackName} (${region})`);
3597
3840
  } catch (error) {
3598
3841
  throw new LockError(
3599
- `Failed to release lock for stack '${stackName}': ${error instanceof Error ? error.message : String(error)}`,
3842
+ `Failed to release lock for stack '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`,
3600
3843
  error instanceof Error ? error : void 0
3601
3844
  );
3602
3845
  }
@@ -3606,23 +3849,28 @@ var LockManager = class {
3606
3849
  *
3607
3850
  * This is intended for CLI usage (e.g., --force-unlock flag) when a lock
3608
3851
  * is stuck and needs manual intervention.
3852
+ *
3853
+ * Pass `region: undefined` to operate on a legacy
3854
+ * `{prefix}/{stackName}/lock.json` file.
3609
3855
  */
3610
- async forceReleaseLock(stackName) {
3611
- const lockInfo = await this.getLockInfo(stackName);
3856
+ async forceReleaseLock(stackName, region) {
3857
+ const lockInfo = await this.getLockInfo(stackName, region);
3612
3858
  if (!lockInfo) {
3613
- this.logger.warn(`No lock to force release for stack: ${stackName}`);
3859
+ this.logger.warn(
3860
+ `No lock to force release for stack: ${stackName}${region ? ` (${region})` : ""}`
3861
+ );
3614
3862
  return;
3615
3863
  }
3616
3864
  this.logger.warn(
3617
- `Force releasing lock for stack: ${stackName}, owner: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expired: ${this.isLockExpired(lockInfo)}`
3865
+ `Force releasing lock for stack: ${stackName}${region ? ` (${region})` : ""}, owner: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expired: ${this.isLockExpired(lockInfo)}`
3618
3866
  );
3619
- await this.deleteLock(stackName);
3867
+ await this.deleteLock(stackName, region);
3620
3868
  }
3621
3869
  /**
3622
3870
  * Internal method to delete the lock file from S3
3623
3871
  */
3624
- async deleteLock(stackName) {
3625
- const key = this.getLockKey(stackName);
3872
+ async deleteLock(stackName, region) {
3873
+ const key = this.getLockKey(stackName, region);
3626
3874
  await this.s3Client.send(
3627
3875
  new DeleteObjectCommand2({
3628
3876
  Bucket: this.config.bucket,
@@ -3643,28 +3891,28 @@ var LockManager = class {
3643
3891
  * @param maxRetries Maximum number of retries (default: 3)
3644
3892
  * @param retryDelay Delay between retries in milliseconds (default: 2000)
3645
3893
  */
3646
- async acquireLockWithRetry(stackName, owner, operation, maxRetries = 3, retryDelay = 2e3) {
3894
+ async acquireLockWithRetry(stackName, region, owner, operation, maxRetries = 3, retryDelay = 2e3) {
3647
3895
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
3648
- const acquired = await this.acquireLock(stackName, owner, operation);
3896
+ const acquired = await this.acquireLock(stackName, region, owner, operation);
3649
3897
  if (acquired) {
3650
3898
  return;
3651
3899
  }
3652
- const lockInfo2 = await this.getLockInfo(stackName);
3900
+ const lockInfo2 = await this.getLockInfo(stackName, region);
3653
3901
  if (lockInfo2) {
3654
3902
  const remainingMs = lockInfo2.expiresAt - Date.now();
3655
3903
  if (attempt < maxRetries) {
3656
3904
  this.logger.info(
3657
- `Stack '${stackName}' is locked by ${lockInfo2.owner}${lockInfo2.operation ? ` (operation: ${lockInfo2.operation})` : ""}. Lock expires in ${this.formatDuration(remainingMs)}. Retrying in ${this.formatDuration(retryDelay)}... (attempt ${attempt + 1}/${maxRetries})`
3905
+ `Stack '${stackName}' (${region}) is locked by ${lockInfo2.owner}${lockInfo2.operation ? ` (operation: ${lockInfo2.operation})` : ""}. Lock expires in ${this.formatDuration(remainingMs)}. Retrying in ${this.formatDuration(retryDelay)}... (attempt ${attempt + 1}/${maxRetries})`
3658
3906
  );
3659
3907
  await new Promise((resolve4) => setTimeout(resolve4, retryDelay));
3660
3908
  continue;
3661
3909
  }
3662
3910
  }
3663
3911
  }
3664
- const lockInfo = await this.getLockInfo(stackName);
3912
+ const lockInfo = await this.getLockInfo(stackName, region);
3665
3913
  const expiresIn = lockInfo ? this.formatDuration(lockInfo.expiresAt - Date.now()) : "unknown";
3666
3914
  throw new LockError(
3667
- `Failed to acquire lock for stack '${stackName}' after ${maxRetries + 1} attempts. ` + (lockInfo ? `Locked by: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expires in: ${expiresIn}. Use --force-unlock to manually release the lock.` : "Lock exists but could not read lock info.")
3915
+ `Failed to acquire lock for stack '${stackName}' (${region}) after ${maxRetries + 1} attempts. ` + (lockInfo ? `Locked by: ${lockInfo.owner}${lockInfo.operation ? `, operation: ${lockInfo.operation}` : ""}, expires in: ${expiresIn}. Use --force-unlock to manually release the lock.` : "Lock exists but could not read lock info.")
3668
3916
  );
3669
3917
  }
3670
3918
  };
@@ -5447,35 +5695,45 @@ var IntrinsicFunctionResolver = class {
5447
5695
  }
5448
5696
  this.logger.debug(`Resolving Fn::ImportValue: ${exportName}`);
5449
5697
  const allStacks = await context.stateBackend.listStacks();
5450
- this.logger.debug(`Found ${allStacks.length} stacks to search for export: ${exportName}`);
5451
- for (const stackName of allStacks) {
5452
- if (context.stackName && stackName === context.stackName) {
5453
- this.logger.debug(`Skipping current stack: ${stackName}`);
5698
+ this.logger.debug(
5699
+ `Found ${allStacks.length} state record(s) to search for export: ${exportName}`
5700
+ );
5701
+ for (const ref of allStacks) {
5702
+ const { stackName: refStack, region: refRegion } = ref;
5703
+ if (context.stackName && refStack === context.stackName) {
5704
+ this.logger.debug(`Skipping current stack: ${refStack}`);
5454
5705
  continue;
5455
5706
  }
5456
5707
  try {
5457
- const stateData = await context.stateBackend.getState(stackName);
5708
+ const lookupRegion = refRegion ?? this.resolverRegion ?? "";
5709
+ if (!lookupRegion) {
5710
+ this.logger.debug(
5711
+ `No region available for stack '${refStack}' \u2014 skipping (cdkd cannot read state without a region)`
5712
+ );
5713
+ continue;
5714
+ }
5715
+ const stateData = await context.stateBackend.getState(refStack, lookupRegion);
5458
5716
  if (!stateData) {
5459
- this.logger.debug(`No state found for stack: ${stackName}`);
5717
+ this.logger.debug(`No state found for stack: ${refStack} (${lookupRegion})`);
5460
5718
  continue;
5461
5719
  }
5462
5720
  const { state } = stateData;
5463
5721
  if (state.outputs && exportName in state.outputs) {
5464
5722
  const value = state.outputs[exportName];
5465
5723
  this.logger.info(
5466
- `Resolved Fn::ImportValue: ${exportName} = ${JSON.stringify(value)} (from stack: ${stackName})`
5724
+ `Resolved Fn::ImportValue: ${exportName} = ${JSON.stringify(value)} (from stack: ${refStack} / ${lookupRegion})`
5467
5725
  );
5468
5726
  return value;
5469
5727
  }
5470
5728
  } catch (error) {
5471
5729
  this.logger.warn(
5472
- `Failed to read state for stack ${stackName}: ${error instanceof Error ? error.message : String(error)}`
5730
+ `Failed to read state for stack ${refStack}: ${error instanceof Error ? error.message : String(error)}`
5473
5731
  );
5474
5732
  continue;
5475
5733
  }
5476
5734
  }
5477
5735
  throw new Error(
5478
- `Fn::ImportValue: export '${exportName}' not found in any stack. Searched ${allStacks.length} stacks. Make sure the exporting stack has been deployed and the Output has an Export.Name property.`
5736
+ `Fn::ImportValue: export '${exportName}' not found in any stack. Searched ${allStacks.length} state record(s). Make sure the exporting stack has been deployed and the Output has an Export.Name property.`
5479
5737
  );
5480
5738
  }
5481
5739
  /**
@@ -26256,7 +26514,11 @@ var DeployEngine = class {
26256
26514
  logger = getLogger().child("DeployEngine");
26257
26515
  resolver;
26258
26516
  interrupted = false;
26259
- /** Target region for this stack (saved in state for cross-region destroy) */
26517
+ /**
26518
+ * Target region for this stack. Required — load-bearing for the
26519
+ * region-prefixed S3 state key and recorded in state.json for
26520
+ * cross-region destroy.
26521
+ */
26260
26522
  stackRegion;
26261
26523
  /**
26262
26524
  * Deploy a CloudFormation template
@@ -26265,7 +26527,7 @@ var DeployEngine = class {
26265
26527
  const startTime = Date.now();
26266
26528
  this.logger.debug(`Starting deployment for stack: ${stackName}`);
26267
26529
  setCurrentStackName(stackName);
26268
- await this.lockManager.acquireLockWithRetry(stackName, void 0, "deploy");
26530
+ await this.lockManager.acquireLockWithRetry(stackName, this.stackRegion, void 0, "deploy");
26269
26531
  const renderer = getLiveRenderer();
26270
26532
  renderer.start();
26271
26533
  this.interrupted = false;
@@ -26279,16 +26541,17 @@ var DeployEngine = class {
26279
26541
  };
26280
26542
  process.on("SIGINT", sigintHandler);
26281
26543
  try {
26282
- const currentStateData = await this.stateBackend.getState(stackName);
26544
+ const currentStateData = await this.stateBackend.getState(stackName, this.stackRegion);
26283
26545
  const currentState = currentStateData?.state ?? {
26284
- version: 1,
26285
- ...this.stackRegion && { region: this.stackRegion },
26546
+ version: STATE_SCHEMA_VERSION_CURRENT,
26547
+ region: this.stackRegion,
26286
26548
  stackName,
26287
26549
  resources: {},
26288
26550
  outputs: {},
26289
26551
  lastModified: Date.now()
26290
26552
  };
26291
26553
  const currentEtag = currentStateData?.etag;
26554
+ const migrationPending = currentStateData?.migrationPending ?? false;
26292
26555
  this.logger.debug(
26293
26556
  `Loaded current state: ${Object.keys(currentState.resources).length} resources`
26294
26557
  );
@@ -26374,9 +26637,10 @@ var DeployEngine = class {
26374
26637
  parameterValues,
26375
26638
  conditions,
26376
26639
  currentEtag,
26377
- progress
26640
+ progress,
26641
+ migrationPending
26378
26642
  );
26379
- const newEtag = await this.stateBackend.saveState(stackName, newState);
26643
+ const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, newState);
26380
26644
  this.logger.debug(`State saved (ETag: ${newEtag})`);
26381
26645
  const durationMs = Date.now() - startTime;
26382
26646
  const unchangedCount = this.diffCalculator.filterByType(changes, "NO_CHANGE").length + actualCounts.skipped;
@@ -26392,7 +26656,7 @@ var DeployEngine = class {
26392
26656
  renderer.stop();
26393
26657
  process.removeListener("SIGINT", sigintHandler);
26394
26658
  try {
26395
- await this.lockManager.releaseLock(stackName);
26659
+ await this.lockManager.releaseLock(stackName, this.stackRegion);
26396
26660
  this.logger.debug("Lock released");
26397
26661
  } catch (lockError) {
26398
26662
  this.logger.warn(
@@ -26410,11 +26674,12 @@ var DeployEngine = class {
26410
26674
  * - DELETE follows reverse dependency order (a node starts as soon as all
26411
26675
  * resources that depend ON it have finished deleting)
26412
26676
  */
26413
- async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
26677
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
26414
26678
  const concurrency = this.options.concurrency;
26415
26679
  const newResources = { ...currentState.resources };
26416
26680
  const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
26417
26681
  const completedOperations = [];
26682
+ let pendingMigration = migrationPending;
26418
26683
  let saveChain = Promise.resolve();
26419
26684
  const saveStateAfterResource = (logicalId) => {
26420
26685
  if (currentEtag === void 0)
@@ -26422,14 +26687,23 @@ var DeployEngine = class {
26422
26687
  saveChain = saveChain.then(async () => {
26423
26688
  try {
26424
26689
  const partialState = {
26425
- version: 1,
26426
- ...this.stackRegion && { region: this.stackRegion },
26690
+ version: STATE_SCHEMA_VERSION_CURRENT,
26691
+ region: this.stackRegion,
26427
26692
  stackName: currentState.stackName,
26428
26693
  resources: newResources,
26429
26694
  outputs: currentState.outputs,
26430
26695
  lastModified: Date.now()
26431
26696
  };
26432
- currentEtag = await this.stateBackend.saveState(stackName, partialState, currentEtag);
26697
+ const migrate = pendingMigration;
26698
+ const expectedEtag = migrate ? void 0 : currentEtag;
26699
+ currentEtag = await this.stateBackend.saveState(
26700
+ stackName,
26701
+ this.stackRegion,
26702
+ partialState,
26703
+ { ...expectedEtag !== void 0 && { expectedEtag }, migrateLegacy: migrate }
26704
+ );
26705
+ if (migrate)
26706
+ pendingMigration = false;
26433
26707
  this.logger.debug(`State saved after ${logicalId}`);
26434
26708
  } catch (error) {
26435
26709
  this.logger.warn(
@@ -26563,14 +26837,23 @@ var DeployEngine = class {
26563
26837
  } catch (error) {
26564
26838
  try {
26565
26839
  const preRollbackState = {
26566
- version: 1,
26567
- ...this.stackRegion && { region: this.stackRegion },
26840
+ version: STATE_SCHEMA_VERSION_CURRENT,
26841
+ region: this.stackRegion,
26568
26842
  stackName: currentState.stackName,
26569
26843
  resources: newResources,
26570
26844
  outputs: currentState.outputs,
26571
26845
  lastModified: Date.now()
26572
26846
  };
26573
- currentEtag = await this.stateBackend.saveState(stackName, preRollbackState, currentEtag);
26847
+ const migrate = pendingMigration;
26848
+ const expectedEtag = migrate ? void 0 : currentEtag;
26849
+ currentEtag = await this.stateBackend.saveState(
26850
+ stackName,
26851
+ this.stackRegion,
26852
+ preRollbackState,
26853
+ { ...expectedEtag !== void 0 && { expectedEtag }, migrateLegacy: migrate }
26854
+ );
26855
+ if (migrate)
26856
+ pendingMigration = false;
26574
26857
  this.logger.debug("Partial state saved before rollback (orphaned resource tracking)");
26575
26858
  } catch (saveError) {
26576
26859
  this.logger.warn(
@@ -26591,31 +26874,35 @@ var DeployEngine = class {
26591
26874
  }
26592
26875
  try {
26593
26876
  const postRollbackState = {
26594
- version: 1,
26595
- ...this.stackRegion && { region: this.stackRegion },
26877
+ version: STATE_SCHEMA_VERSION_CURRENT,
26878
+ region: this.stackRegion,
26596
26879
  stackName: currentState.stackName,
26597
26880
  resources: newResources,
26598
26881
  outputs: currentState.outputs,
26599
26882
  lastModified: Date.now()
26600
26883
  };
26601
- await this.stateBackend.saveState(stackName, postRollbackState, currentEtag);
26884
+ await this.stateBackend.saveState(stackName, this.stackRegion, postRollbackState, {
26885
+ ...currentEtag !== void 0 && { expectedEtag: currentEtag }
26886
+ });
26602
26887
  this.logger.debug("State saved after deployment failure");
26603
26888
  } catch (saveError) {
26604
26889
  this.logger.debug(
26605
26890
  `Retrying state save after rollback (ETag mismatch): ${saveError instanceof Error ? saveError.message : String(saveError)}`
26606
26891
  );
26607
26892
  try {
26608
- const freshState = await this.stateBackend.getState(stackName);
26893
+ const freshState = await this.stateBackend.getState(stackName, this.stackRegion);
26609
26894
  const freshEtag = freshState?.etag;
26610
26895
  const postRollbackState = {
26611
- version: 1,
26612
- ...this.stackRegion && { region: this.stackRegion },
26896
+ version: STATE_SCHEMA_VERSION_CURRENT,
26897
+ region: this.stackRegion,
26613
26898
  stackName: currentState.stackName,
26614
26899
  resources: newResources,
26615
26900
  outputs: currentState.outputs,
26616
26901
  lastModified: Date.now()
26617
26902
  };
26618
- await this.stateBackend.saveState(stackName, postRollbackState, freshEtag);
26903
+ await this.stateBackend.saveState(stackName, this.stackRegion, postRollbackState, {
26904
+ ...freshEtag !== void 0 && { expectedEtag: freshEtag }
26905
+ });
26619
26906
  this.logger.debug("State saved after deployment failure (retry succeeded)");
26620
26907
  } catch (retryError) {
26621
26908
  this.logger.warn(
@@ -26634,8 +26921,8 @@ var DeployEngine = class {
26634
26921
  );
26635
26922
  return {
26636
26923
  state: {
26637
- version: 1,
26638
- ...this.stackRegion && { region: this.stackRegion },
26924
+ version: STATE_SCHEMA_VERSION_CURRENT,
26925
+ region: this.stackRegion,
26639
26926
  stackName: currentState.stackName,
26640
26927
  resources: newResources,
26641
26928
  outputs,
@@ -27642,19 +27929,18 @@ async function diffCommand(stacks, options) {
27642
27929
  logger.info(`
27643
27930
  Calculating diff for stack: ${stackInfo.stackName}`);
27644
27931
  const template = stackInfo.template;
27645
- let currentState;
27646
- const stateResult = await stateBackend.getState(stackInfo.stackName);
27647
- if (stateResult) {
27648
- currentState = stateResult.state;
27649
- } else {
27650
- logger.debug(`No existing state for ${stackInfo.stackName}`);
27651
- currentState = {
27652
- stackName: stackInfo.stackName,
27653
- resources: {},
27654
- outputs: {},
27655
- version: 1,
27656
- lastModified: Date.now()
27657
- };
27932
+ const stackRegion = stackInfo.region || region;
27933
+ const stateResult = await stateBackend.getState(stackInfo.stackName, stackRegion);
27934
+ const currentState = stateResult ? stateResult.state : {
27935
+ stackName: stackInfo.stackName,
27936
+ region: stackRegion,
27937
+ resources: {},
27938
+ outputs: {},
27939
+ version: STATE_SCHEMA_VERSION_CURRENT,
27940
+ lastModified: Date.now()
27941
+ };
27942
+ if (!stateResult) {
27943
+ logger.debug(`No existing state for ${stackInfo.stackName} (${stackRegion})`);
27658
27944
  }
27659
27945
  const diffResolveFn = (value) => intrinsicResolver.resolve(value, {
27660
27946
  template,
@@ -27776,19 +28062,27 @@ async function destroyCommand(stackArgs, options) {
27776
28062
  });
27777
28063
  appStacks = result.stacks.map((s) => ({
27778
28064
  stackName: s.stackName,
27779
- displayName: s.displayName
28065
+ displayName: s.displayName,
28066
+ ...s.region && { region: s.region }
27780
28067
  }));
27781
28068
  } catch {
27782
28069
  logger.debug("Could not synthesize app, falling back to state-based stack list");
27783
28070
  }
27784
28071
  }
27785
- const allStateStacks = await stateBackend.listStacks();
28072
+ const allStateRefs = await stateBackend.listStacks();
27786
28073
  let candidateStacks;
27787
28074
  if (appStacks.length > 0) {
27788
- const stateSet = new Set(allStateStacks);
27789
- candidateStacks = appStacks.filter((s) => stateSet.has(s.stackName));
28075
+ const stateNames = new Set(allStateRefs.map((r) => r.stackName));
28076
+ candidateStacks = appStacks.filter((s) => stateNames.has(s.stackName));
27790
28077
  } else if (stackArgs.length > 0 || options.stack || options.all) {
27791
- candidateStacks = allStateStacks.map((name) => ({ stackName: name }));
28078
+ const seen = /* @__PURE__ */ new Set();
28079
+ candidateStacks = [];
28080
+ for (const ref of allStateRefs) {
28081
+ if (seen.has(ref.stackName))
28082
+ continue;
28083
+ seen.add(ref.stackName);
28084
+ candidateStacks.push({ stackName: ref.stackName });
28085
+ }
27792
28086
  } else {
27793
28087
  throw new Error(
27794
28088
  "Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured."
@@ -27815,10 +28109,38 @@ async function destroyCommand(stackArgs, options) {
27815
28109
  return;
27816
28110
  }
27817
28111
  logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
28112
+ const stateRefsByName = /* @__PURE__ */ new Map();
28113
+ for (const ref of allStateRefs) {
28114
+ const arr = stateRefsByName.get(ref.stackName) ?? [];
28115
+ arr.push(ref);
28116
+ stateRefsByName.set(ref.stackName, arr);
28117
+ }
27818
28118
  for (const stackName of stackNames) {
27819
28119
  logger.info(`
27820
28120
  Preparing to destroy stack: ${stackName}`);
27821
- const stateResult = await stateBackend.getState(stackName);
28121
+ const refs = stateRefsByName.get(stackName) ?? [];
28122
+ const synthStack = appStacks.find((s) => s.stackName === stackName);
28123
+ const synthRegion = synthStack?.region;
28124
+ let stackTargetRegion;
28125
+ if (refs.length === 0) {
28126
+ logger.warn(`No state found for stack ${stackName}, skipping`);
28127
+ continue;
28128
+ } else if (refs.length === 1) {
28129
+ const onlyRegion = refs[0]?.region;
28130
+ if (!onlyRegion) {
28131
+ stackTargetRegion = region;
28132
+ } else {
28133
+ stackTargetRegion = onlyRegion;
28134
+ }
28135
+ } else if (synthRegion && refs.some((r) => r.region === synthRegion)) {
28136
+ stackTargetRegion = synthRegion;
28137
+ } else {
28138
+ const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
28139
+ throw new Error(
28140
+ `Stack '${stackName}' has state in multiple regions: ${regions}. Use 'cdkd state rm ${stackName} --region <region>' to remove cdkd's record for one region, or run destroy from a CDK app whose env.region matches one of them.`
28141
+ );
28142
+ }
28143
+ const stateResult = await stateBackend.getState(stackName, stackTargetRegion);
27822
28144
  if (!stateResult) {
27823
28145
  logger.warn(`No state found for stack ${stackName}, skipping`);
27824
28146
  continue;
@@ -27827,7 +28149,7 @@ Preparing to destroy stack: ${stackName}`);
27827
28149
  const resourceCount = Object.keys(currentState.resources).length;
27828
28150
  if (resourceCount === 0) {
27829
28151
  logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
27830
- await stateBackend.deleteState(stackName);
28152
+ await stateBackend.deleteState(stackName, stackTargetRegion);
27831
28153
  logger.info("\u2713 State deleted");
27832
28154
  continue;
27833
28155
  }
@@ -27852,7 +28174,7 @@ Are you sure you want to destroy stack "${stackName}" and delete all ${resourceC
27852
28174
  continue;
27853
28175
  }
27854
28176
  }
27855
- const stackRegion = currentState.region;
28177
+ const stackRegion = stackTargetRegion;
27856
28178
  let destroyProviderRegistry = providerRegistry;
27857
28179
  let destroyAwsClients;
27858
28180
  if (stackRegion && stackRegion !== region) {
@@ -27870,7 +28192,7 @@ Are you sure you want to destroy stack "${stackName}" and delete all ${resourceC
27870
28192
  }
27871
28193
  logger.info(`
27872
28194
  Acquiring lock for stack ${stackName}...`);
27873
- await lockManager.acquireLock(stackName, "destroy");
28195
+ await lockManager.acquireLock(stackName, stackRegion, void 0, "destroy");
27874
28196
  const renderer = getLiveRenderer();
27875
28197
  renderer.start();
27876
28198
  try {
@@ -27985,7 +28307,7 @@ Acquiring lock for stack ${stackName}...`);
27985
28307
  await Promise.all(deletePromises);
27986
28308
  }
27987
28309
  if (errorCount === 0) {
27988
- await stateBackend.deleteState(stackName);
28310
+ await stateBackend.deleteState(stackName, stackRegion);
27989
28311
  logger.debug("State deleted");
27990
28312
  } else {
27991
28313
  logger.warn(`${errorCount} resource(s) failed to delete. State preserved.`);
@@ -27997,7 +28319,7 @@ Acquiring lock for stack ${stackName}...`);
27997
28319
  } finally {
27998
28320
  renderer.stop();
27999
28321
  logger.debug("Releasing lock...");
28000
- await lockManager.releaseLock(stackName);
28322
+ await lockManager.releaseLock(stackName, stackRegion);
28001
28323
  if (destroyAwsClients) {
28002
28324
  destroyAwsClients.destroy();
28003
28325
  process.env["AWS_REGION"] = region;
@@ -28058,7 +28380,7 @@ function createPublishAssetsCommand() {
28058
28380
  }
28059
28381
 
28060
28382
  // src/cli/commands/force-unlock.ts
28061
- import { Command as Command8 } from "commander";
28383
+ import { Command as Command8, Option as Option3 } from "commander";
28062
28384
  init_aws_clients();
28063
28385
  async function forceUnlockCommand(stackArgs, options) {
28064
28386
  const logger = getLogger();
@@ -28082,17 +28404,33 @@ async function forceUnlockCommand(stackArgs, options) {
28082
28404
  prefix: options.statePrefix
28083
28405
  };
28084
28406
  const lockManager = new LockManager(awsClients.s3, stateConfig);
28407
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig);
28085
28408
  for (const stackName of stackPatterns) {
28086
- logger.info(`Force-unlocking stack: ${stackName}`);
28087
- try {
28088
- await lockManager.forceReleaseLock(stackName);
28089
- logger.info(`\u2713 Lock released for stack: ${stackName}`);
28090
- } catch (error) {
28091
- const message = error instanceof Error ? error.message : String(error);
28092
- if (message.includes("No lock found") || message.includes("NoSuchKey")) {
28093
- logger.info(`No lock found for stack: ${stackName}`);
28409
+ let regionsToTry;
28410
+ if (options.stackRegion) {
28411
+ regionsToTry = [options.stackRegion];
28412
+ } else {
28413
+ const refs = await stateBackend.listStacks();
28414
+ const matched = refs.filter((r) => r.stackName === stackName);
28415
+ if (matched.length === 0) {
28416
+ regionsToTry = [region];
28094
28417
  } else {
28095
- logger.error(`Failed to unlock stack ${stackName}: ${message}`);
28418
+ regionsToTry = matched.map((r) => r.region);
28419
+ }
28420
+ }
28421
+ for (const r of regionsToTry) {
28422
+ const where = r ? `${stackName} (${r})` : `${stackName} (legacy lock key)`;
28423
+ logger.info(`Force-unlocking stack: ${where}`);
28424
+ try {
28425
+ await lockManager.forceReleaseLock(stackName, r);
28426
+ logger.info(`\u2713 Lock released for stack: ${where}`);
28427
+ } catch (error) {
28428
+ const message = error instanceof Error ? error.message : String(error);
28429
+ if (message.includes("No lock found") || message.includes("NoSuchKey")) {
28430
+ logger.info(`No lock found for stack: ${where}`);
28431
+ } else {
28432
+ logger.error(`Failed to unlock stack ${where}: ${message}`);
28433
+ }
28096
28434
  }
28097
28435
  }
28098
28436
  }
@@ -28101,15 +28439,47 @@ async function forceUnlockCommand(stackArgs, options) {
28101
28439
  }
28102
28440
  }
28103
28441
  function createForceUnlockCommand() {
28104
- const cmd = new Command8("force-unlock").description("Force-release a stale lock on a stack").argument("[stacks...]", "Stack name(s) to unlock").action(withErrorHandling(forceUnlockCommand));
28442
+ const cmd = new Command8("force-unlock").description("Force-release a stale lock on a stack").argument("[stacks...]", "Stack name(s) to unlock").addOption(
28443
+ new Option3(
28444
+ "--stack-region <region>",
28445
+ "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."
28446
+ )
28447
+ ).action(withErrorHandling(forceUnlockCommand));
28105
28448
  [...commonOptions, ...stateOptions, ...stackOptions].forEach((opt) => cmd.addOption(opt));
28106
28449
  return cmd;
28107
28450
  }
28108
28451
 
28109
28452
  // src/cli/commands/state.ts
28110
28453
  import * as readline2 from "node:readline/promises";
28111
- import { Command as Command9 } from "commander";
28454
+ import { Command as Command9, Option as Option4 } from "commander";
28112
28455
  init_aws_clients();
28456
+ function formatStackRef(ref) {
28457
+ return ref.region ? `${ref.stackName} (${ref.region})` : ref.stackName;
28458
+ }
28459
+ function resolveSingleRegion(stackName, refs, requestedRegion) {
28460
+ const matches = refs.filter((r) => r.stackName === stackName);
28461
+ if (matches.length === 0) {
28462
+ throw new Error(
28463
+ `No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
28464
+ );
28465
+ }
28466
+ if (requestedRegion) {
28467
+ const ref = matches.find((r) => r.region === requestedRegion);
28468
+ if (!ref) {
28469
+ const seen = matches.map((r) => r.region ?? "(legacy)").join(", ");
28470
+ throw new Error(
28471
+ `No state found for stack '${stackName}' in region '${requestedRegion}'. Available regions: ${seen}.`
28472
+ );
28473
+ }
28474
+ return ref;
28475
+ }
28476
+ if (matches.length === 1)
28477
+ return matches[0];
28478
+ const regions = matches.map((r) => r.region ?? "(legacy)").join(", ");
28479
+ throw new Error(
28480
+ `Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --region <region> to disambiguate.`
28481
+ );
28482
+ }
28113
28483
  async function setupStateBackend(options) {
28114
28484
  const awsClients = new AwsClients({
28115
28485
  ...options.region && { region: options.region },
@@ -28131,34 +28501,52 @@ async function setupStateBackend(options) {
28131
28501
  dispose: () => awsClients.destroy()
28132
28502
  };
28133
28503
  }
28504
+ function sortRefs(refs) {
28505
+ return refs.slice().sort((a, b) => {
28506
+ if (a.stackName < b.stackName)
28507
+ return -1;
28508
+ if (a.stackName > b.stackName)
28509
+ return 1;
28510
+ const ar = a.region ?? "\uFFFF";
28511
+ const br = b.region ?? "\uFFFF";
28512
+ if (ar < br)
28513
+ return -1;
28514
+ if (ar > br)
28515
+ return 1;
28516
+ return 0;
28517
+ });
28518
+ }
28134
28519
  async function stateListCommand(options) {
28135
28520
  const logger = getLogger();
28136
28521
  if (options.verbose)
28137
28522
  logger.setLevel("debug");
28138
28523
  const setup = await setupStateBackend(options);
28139
28524
  try {
28140
- const stackNames = (await setup.stateBackend.listStacks()).slice().sort();
28525
+ const refs = sortRefs(await setup.stateBackend.listStacks());
28141
28526
  if (!options.long && !options.json) {
28142
- for (const name of stackNames) {
28143
- process.stdout.write(`${name}
28527
+ for (const ref of refs) {
28528
+ process.stdout.write(`${formatStackRef(ref)}
28144
28529
  `);
28145
28530
  }
28146
28531
  return;
28147
28532
  }
28148
28533
  if (options.json && !options.long) {
28149
- process.stdout.write(`${JSON.stringify(stackNames, null, 2)}
28534
+ const payload = refs.map((r) => ({ stackName: r.stackName, region: r.region ?? null }));
28535
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
28150
28536
  `);
28151
28537
  return;
28152
28538
  }
28153
28539
  const details = await Promise.all(
28154
- stackNames.map(async (stackName) => {
28540
+ refs.map(async (ref) => {
28541
+ const lookupRegion = ref.region ?? "";
28155
28542
  const [stateResult, locked] = await Promise.all([
28156
- setup.stateBackend.getState(stackName),
28157
- setup.lockManager.isLocked(stackName)
28543
+ lookupRegion ? setup.stateBackend.getState(ref.stackName, lookupRegion) : Promise.resolve(null),
28544
+ setup.lockManager.isLocked(ref.stackName, ref.region)
28158
28545
  ]);
28159
28546
  const state = stateResult?.state;
28160
28547
  return {
28161
- stackName,
28548
+ stackName: ref.stackName,
28549
+ region: ref.region ?? null,
28162
28550
  resourceCount: state ? Object.keys(state.resources).length : 0,
28163
28551
  lastModified: state && typeof state.lastModified === "number" ? new Date(state.lastModified).toISOString() : null,
28164
28552
  locked
@@ -28172,7 +28560,13 @@ async function stateListCommand(options) {
28172
28560
  }
28173
28561
  const lines = [];
28174
28562
  for (const detail of details) {
28175
- lines.push(detail.stackName);
28563
+ lines.push(
28564
+ formatStackRef({
28565
+ stackName: detail.stackName,
28566
+ ...detail.region ? { region: detail.region } : {}
28567
+ })
28568
+ );
28569
+ lines.push(` Region: ${detail.region ?? "(legacy)"}`);
28176
28570
  lines.push(` Resources: ${detail.resourceCount}`);
28177
28571
  lines.push(` Last Modified: ${detail.lastModified ?? "unknown"}`);
28178
28572
  lines.push(` Lock: ${detail.locked ? "locked" : "unlocked"}`);
@@ -28200,10 +28594,17 @@ async function stateResourcesCommand(stackName, options) {
28200
28594
  logger.setLevel("debug");
28201
28595
  const setup = await setupStateBackend(options);
28202
28596
  try {
28203
- const stateResult = await setup.stateBackend.getState(stackName);
28597
+ const refs = await setup.stateBackend.listStacks();
28598
+ const ref = resolveSingleRegion(stackName, refs, options.stackRegion);
28599
+ if (!ref.region) {
28600
+ throw new Error(
28601
+ `Stack '${stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${stackName}' (or any cdkd write) to migrate it to the region-scoped layout, then re-run this command.`
28602
+ );
28603
+ }
28604
+ const stateResult = await setup.stateBackend.getState(stackName, ref.region);
28204
28605
  if (!stateResult) {
28205
28606
  throw new Error(
28206
- `No state found for stack '${stackName}' in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28607
+ `No state found for stack '${stackName}' (${ref.region}) in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28207
28608
  );
28208
28609
  }
28209
28610
  const resources = stateResult.state.resources ?? {};
@@ -28286,7 +28687,7 @@ function formatLockSummary(lockInfo) {
28286
28687
  return `locked by ${lockInfo.owner}${opStr}, ${expiresStr}`;
28287
28688
  }
28288
28689
  function createStateResourcesCommand() {
28289
- 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).action(withErrorHandling(stateResourcesCommand));
28690
+ 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));
28290
28691
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28291
28692
  return cmd;
28292
28693
  }
@@ -28296,13 +28697,20 @@ async function stateShowCommand(stackName, options) {
28296
28697
  logger.setLevel("debug");
28297
28698
  const setup = await setupStateBackend(options);
28298
28699
  try {
28700
+ const refs = await setup.stateBackend.listStacks();
28701
+ const ref = resolveSingleRegion(stackName, refs, options.stackRegion);
28702
+ if (!ref.region) {
28703
+ throw new Error(
28704
+ `Stack '${stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${stackName}' (or any cdkd write) to migrate it to the region-scoped layout, then re-run this command.`
28705
+ );
28706
+ }
28299
28707
  const [stateResult, lockInfo] = await Promise.all([
28300
- setup.stateBackend.getState(stackName),
28301
- setup.lockManager.getLockInfo(stackName)
28708
+ setup.stateBackend.getState(stackName, ref.region),
28709
+ setup.lockManager.getLockInfo(stackName, ref.region)
28302
28710
  ]);
28303
28711
  if (!stateResult) {
28304
28712
  throw new Error(
28305
- `No state found for stack '${stackName}' in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28713
+ `No state found for stack '${stackName}' (${ref.region}) in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`
28306
28714
  );
28307
28715
  }
28308
28716
  if (options.json) {
@@ -28366,7 +28774,7 @@ async function stateShowCommand(stackName, options) {
28366
28774
  }
28367
28775
  }
28368
28776
  function createStateShowCommand() {
28369
- 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).action(withErrorHandling(stateShowCommand));
28777
+ 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));
28370
28778
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28371
28779
  return cmd;
28372
28780
  }
@@ -28379,24 +28787,36 @@ async function stateRmCommand(stackArgs, options) {
28379
28787
  }
28380
28788
  const setup = await setupStateBackend(options);
28381
28789
  try {
28790
+ const refs = await setup.stateBackend.listStacks();
28382
28791
  for (const stackName of stackArgs) {
28383
- const exists = await setup.stateBackend.stateExists(stackName);
28384
- if (!exists) {
28792
+ const stackRefs = refs.filter((r) => r.stackName === stackName);
28793
+ if (stackRefs.length === 0) {
28385
28794
  logger.info(`No state found for stack: ${stackName}, skipping`);
28386
28795
  continue;
28387
28796
  }
28797
+ const targets = options.stackRegion ? stackRefs.filter((r) => r.region === options.stackRegion) : stackRefs;
28798
+ if (targets.length === 0) {
28799
+ const seen = stackRefs.map((r) => r.region ?? "(legacy)").join(", ");
28800
+ throw new Error(
28801
+ `No state found for stack '${stackName}' in region '${options.stackRegion}'. Available regions: ${seen}.`
28802
+ );
28803
+ }
28388
28804
  if (!options.force) {
28389
- const locked = await setup.lockManager.isLocked(stackName);
28390
- if (locked) {
28391
- throw new Error(
28392
- `Stack '${stackName}' is locked. Run 'cdkd force-unlock ${stackName}' first, or pass --force to remove anyway.`
28393
- );
28805
+ for (const target of targets) {
28806
+ const locked = await setup.lockManager.isLocked(stackName, target.region);
28807
+ if (locked) {
28808
+ const where = target.region ?? "(legacy)";
28809
+ throw new Error(
28810
+ `Stack '${stackName}' (${where}) is locked. Run 'cdkd force-unlock ${stackName}${target.region ? ` --stack-region ${target.region}` : ""}' first, or pass --force to remove anyway.`
28811
+ );
28812
+ }
28394
28813
  }
28395
28814
  }
28396
28815
  if (!options.yes && !options.force) {
28816
+ const targetList = targets.map((t) => formatStackRef(t)).join(", ");
28397
28817
  process.stdout.write(
28398
28818
  `
28399
- WARNING: This removes cdkd's state record for '${stackName}' only. AWS resources will NOT be deleted.
28819
+ WARNING: This removes cdkd's state record for [${targetList}] only. AWS resources will NOT be deleted.
28400
28820
  Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
28401
28821
 
28402
28822
  `
@@ -28406,7 +28826,7 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
28406
28826
  output: process.stdout
28407
28827
  });
28408
28828
  const answer = await rl.question(
28409
- `Remove state for stack '${stackName}' from s3://${setup.bucket}/${setup.prefix}/? (y/N): `
28829
+ `Remove state for ${targetList} from s3://${setup.bucket}/${setup.prefix}/? (y/N): `
28410
28830
  );
28411
28831
  rl.close();
28412
28832
  const trimmed = answer.trim().toLowerCase();
@@ -28415,16 +28835,28 @@ Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
28415
28835
  continue;
28416
28836
  }
28417
28837
  }
28418
- await setup.stateBackend.deleteState(stackName);
28419
- await setup.lockManager.forceReleaseLock(stackName);
28420
- logger.info(`\u2713 Removed state for stack: ${stackName}`);
28838
+ for (const target of targets) {
28839
+ if (target.region) {
28840
+ await setup.stateBackend.deleteState(stackName, target.region);
28841
+ await setup.lockManager.forceReleaseLock(stackName, target.region);
28842
+ } else {
28843
+ await setup.lockManager.forceReleaseLock(stackName, void 0);
28844
+ }
28845
+ logger.info(`\u2713 Removed state for stack: ${formatStackRef(target)}`);
28846
+ }
28421
28847
  }
28422
28848
  } finally {
28423
28849
  setup.dispose();
28424
28850
  }
28425
28851
  }
28852
+ function stackRegionOption() {
28853
+ return new Option4(
28854
+ "--stack-region <region>",
28855
+ "Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
28856
+ );
28857
+ }
28426
28858
  function createStateRmCommand() {
28427
- 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).action(withErrorHandling(stateRmCommand));
28859
+ 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));
28428
28860
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
28429
28861
  return cmd;
28430
28862
  }
@@ -28462,7 +28894,7 @@ function reorderArgs(argv) {
28462
28894
  }
28463
28895
  async function main() {
28464
28896
  const program = new Command10();
28465
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.7.0");
28897
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.8.0");
28466
28898
  program.addCommand(createBootstrapCommand());
28467
28899
  program.addCommand(createSynthCommand());
28468
28900
  program.addCommand(createListCommand());