@go-to-k/cdkd 0.284.30 → 0.284.31

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as endCommandInterruptScope, $t as createAssetRedirectResolver, A as MULTI_REGION_RECREATE_BLOCKED_TYPES, An as resolveUseCdkBootstrapAssets, At as maskSecretsInText, B as collectDeclaredOutputNames, Bn as canonicalizeRegion, Bt as INTRINSIC_KEYS, C as ccRoutedFinalSnapshotError, Cn as getLegacyStateBucketName, Ct as STATE_SOURCED_READBACK_RULES, D as unsupportedFinalSnapshotError, Dn as resolveSkipPrefix, Dt as errorCauseChain, E as refusesFinalSnapshot, En as resolveCaptureObservedState, Et as dynamicReferenceTokens, F as cyan, Fn as MIGRATE_TMP_PREFIX, Ft as s3BucketDualStackDomainName, G as stateKeySecretExposure, Gn as resolveBucketRegion, Gt as LockManager, H as exportAliasCollisionScrubWarning, Hn as AssemblyReader, Ht as withRetry, I as gray, In as findLargeInlineResources, It as s3BucketRegionalDomainName, J as clearOnUpdateRemoval, Jn as resetAwsClients, Jt as shouldRetainResource, K as IAMRoleProvider, Kn as AwsClients, Kt as S3StateBackend, L as green, Ln as uploadCfnTemplate, Lt as s3BucketWebsiteUrl, M as renderStatefulReason, Mn as warnDeprecatedNoPrefixCliFlag, Mt as scrubResourceRecord, N as formatResourceLine, Nn as CFN_TEMPLATE_BODY_LIMIT, Nt as s3BucketArn, O as makeCanonicalizePropertiesFn, On as resolveStateBucketWithDefault, Ot as isSingleDynamicReferenceToken, P as bold, Pn as CFN_TEMPLATE_URL_LIMIT, Pt as s3BucketDomainName, Q as beginCommandInterruptScope, Qt as buildAssetRedirectMap, R as red, Rn as expectedOwnerParam, Rt as applyRoleArnIfSet, S as buildFinalSnapshotIdentifier, Sn as getDefaultStateBucketName, Sr as markNonRetryable, St as STATE_SOURCED_CROSS_GENERATION_RULES, T as isFinalSnapshotError, Tn as resolveAutoAssetStorage, Tt as createSecretMasker, U as isExportAliasCollision, Un as processStackMessages, Ut as DagBuilder, V as collectPublishedOutputNames, Vn as derivePartitionAndUrlSuffix, Vt as describeTypeWithThrottleRetry, W as secretBearingStateKeyWarning, Wt as TemplateParser, X as findActionableSilentDrops, Xt as stringifyValue, Y as ProviderRegistry, Yn as setAwsClients, Yt as AssetPublisher, Z as findSilentDropProperties, Zn as CdkdError, Zt as WorkGraph, _ as maskingRetryLogger, _n as runDockerStreaming, _r as normalizeAwsError, _t as readConfigString, a as DeploymentEventsStore, an as BOOTSTRAP_MARKER_PREFIX, ar as MissingCdkCliError, at as isTerminationProtectionPropagationError, b as ATOMIC_FINAL_SNAPSHOT_TYPES, bn as Synthesizer, br as isRetryableTransientError, bt as requireConfigObject, c as planRollback, cn as parseBootstrapMarker, cr as ProvisioningError, ct as getAccountInfo, d as replayRollback, dn as validateContainerRepoName, dr as StackHasActiveImportsError, dt as normalizeAwsTagsToCfn, en as loadPublishableAssetManifest, er as DeployCancelledError, et as isInterruptedWaitError, f as updatePartialMessage, fn as buildDenyExternalAccessPolicy, fr as StackTerminationProtectionError, ft as resolveExplicitPhysicalId, g as withResourceDeadline, gn as runDockerForeground, gt as configStringRefusal, h as deleteSkipReason, hn as getDockerCmd, ht as configBooleanRefusal, i as DeploymentEventsReader, in as AssetModeResolver, it as disableInstanceApiTermination, j as isStatefulRecreateTargetSync, jn as stateBucketExistenceConfirmed, jt as redactSecretsForState, k as extractDeploymentEventError, kn as resolveStateBucketWithDefaultAndSource, kt as maskSecretsInError, l as producerRegionsFromState, ln as readBootstrapMarkerBody, lr as ResourceTimeoutError, lt as refStateLookupFromResource, m as UNSPECIFIED_SKIP_REASON, mn as formatDockerLoginError, mt as coerceCfnBoolean, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as escapeRegExp, nr as LocalMigrateError, nt as CloudControlProvider, o as classifyReplaySecretRegion, on as ensureAssetStorage, or as NestedStackChildDirectDestroyError, ot as IntrinsicFunctionResolver, p as updatePartialReason, pn as buildDockerImage, pt as assertRegionMatch, q as collectInlinePolicyNamesManagedBySiblings, qn as getAwsClients, qt as rebuildClientForBucketRegion, r as DeployEngine, rn as stripControlChars, rr as LocalStartServiceError, rt as slowCcOperationTimeoutMs, s as planFailedOps, sn as getBootstrapMarkerKey, sr as PartialFailureError, st as cfnRefValueFromPhysicalId, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as rewriteTemplateAssetReferences, tr as LocalInvokeBuildError$1, tt as startInterruptWatch, u as replayFailedOperations, un as validateAssetBucketName, ur as ResourceUpdateNotSupportedError, ut as WAFv2WebACLProvider, v as IMPLICIT_DELETE_DEPENDENCIES, vn as AssetManifestLoader, vr as withErrorHandling, vt as replayWarn, w as createPreDeleteFinalSnapshot, wn as resolveApp, wt as TEMPLATE_SOURCED_RULES, x as PRE_DELETE_SNAPSHOT_TYPES, xn as synthesisStatusMessage, xr as isThrottlingError, xt as requireConfigString, y as computeImplicitDeleteEdges, yn as getDockerImageBySourceHash, yr as isMarkedNonRetryable, yt as requireConfigArray, z as yellow, zn as PARTITION_TABLE, zt as DiffCalculator } from "./deploy-engine-BFgi0RJr.js";
2
+ import { $ as endCommandInterruptScope, $t as createAssetRedirectResolver, A as MULTI_REGION_RECREATE_BLOCKED_TYPES, An as resolveUseCdkBootstrapAssets, At as maskSecretsInText, B as collectDeclaredOutputNames, Bn as canonicalizeRegion, Bt as INTRINSIC_KEYS, C as ccRoutedFinalSnapshotError, Cn as getLegacyStateBucketName, Ct as STATE_SOURCED_READBACK_RULES, D as unsupportedFinalSnapshotError, Dn as resolveSkipPrefix, Dt as errorCauseChain, E as refusesFinalSnapshot, En as resolveCaptureObservedState, Et as dynamicReferenceTokens, F as cyan, Fn as MIGRATE_TMP_PREFIX, Ft as s3BucketDualStackDomainName, G as stateKeySecretExposure, Gn as resolveBucketRegion, Gt as LockManager, H as exportAliasCollisionScrubWarning, Hn as AssemblyReader, Ht as withRetry, I as gray, In as findLargeInlineResources, It as s3BucketRegionalDomainName, J as clearOnUpdateRemoval, Jn as resetAwsClients, Jt as shouldRetainResource, K as IAMRoleProvider, Kn as AwsClients, Kt as S3StateBackend, L as green, Ln as uploadCfnTemplate, Lt as s3BucketWebsiteUrl, M as renderStatefulReason, Mn as warnDeprecatedNoPrefixCliFlag, Mt as scrubResourceRecord, N as formatResourceLine, Nn as CFN_TEMPLATE_BODY_LIMIT, Nt as s3BucketArn, O as makeCanonicalizePropertiesFn, On as resolveStateBucketWithDefault, Ot as isSingleDynamicReferenceToken, P as bold, Pn as CFN_TEMPLATE_URL_LIMIT, Pt as s3BucketDomainName, Q as beginCommandInterruptScope, Qt as buildAssetRedirectMap, R as red, Rn as expectedOwnerParam, Rt as applyRoleArnIfSet, S as buildFinalSnapshotIdentifier, Sn as getDefaultStateBucketName, Sr as markNonRetryable, St as STATE_SOURCED_CROSS_GENERATION_RULES, T as isFinalSnapshotError, Tn as resolveAutoAssetStorage, Tt as createSecretMasker, U as isExportAliasCollision, Un as processStackMessages, Ut as DagBuilder, V as collectPublishedOutputNames, Vn as derivePartitionAndUrlSuffix, Vt as describeTypeWithThrottleRetry, W as secretBearingStateKeyWarning, Wt as TemplateParser, X as findActionableSilentDrops, Xt as stringifyValue, Y as ProviderRegistry, Yn as setAwsClients, Yt as AssetPublisher, Z as findSilentDropProperties, Zn as CdkdError, Zt as WorkGraph, _ as maskingRetryLogger, _n as runDockerStreaming, _r as normalizeAwsError, _t as readConfigString, a as DeploymentEventsStore, an as BOOTSTRAP_MARKER_PREFIX, ar as MissingCdkCliError, at as isTerminationProtectionPropagationError, b as ATOMIC_FINAL_SNAPSHOT_TYPES, bn as Synthesizer, br as isRetryableTransientError, bt as requireConfigObject, c as planRollback, cn as parseBootstrapMarker, cr as ProvisioningError, ct as getAccountInfo, d as replayRollback, dn as validateContainerRepoName, dr as StackHasActiveImportsError, dt as normalizeAwsTagsToCfn, en as loadPublishableAssetManifest, er as DeployCancelledError, et as isInterruptedWaitError, f as updatePartialMessage, fn as buildDenyExternalAccessPolicy, fr as StackTerminationProtectionError, ft as resolveExplicitPhysicalId, g as withResourceDeadline, gn as runDockerForeground, gt as configStringRefusal, h as deleteSkipReason, hn as getDockerCmd, ht as configBooleanRefusal, i as DeploymentEventsReader, in as AssetModeResolver, it as disableInstanceApiTermination, j as isStatefulRecreateTargetSync, jn as stateBucketExistenceConfirmed, jt as redactSecretsForState, k as extractDeploymentEventError, kn as resolveStateBucketWithDefaultAndSource, kt as maskSecretsInError, l as producerRegionsFromState, ln as readBootstrapMarkerBody, lr as ResourceTimeoutError, lt as refStateLookupFromResource, m as UNSPECIFIED_SKIP_REASON, mn as formatDockerLoginError, mt as coerceCfnBoolean, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as escapeRegExp, nr as LocalMigrateError, nt as CloudControlProvider, o as classifyReplaySecretRegion, on as ensureAssetStorage, or as NestedStackChildDirectDestroyError, ot as IntrinsicFunctionResolver, p as updatePartialReason, pn as buildDockerImage, pt as assertRegionMatch, q as collectInlinePolicyNamesManagedBySiblings, qn as getAwsClients, qt as rebuildClientForBucketRegion, r as DeployEngine, rn as stripControlChars, rr as LocalStartServiceError, rt as slowCcOperationTimeoutMs, s as planFailedOps, sn as getBootstrapMarkerKey, sr as PartialFailureError, st as cfnRefValueFromPhysicalId, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as rewriteTemplateAssetReferences, tr as LocalInvokeBuildError$1, tt as startInterruptWatch, u as replayFailedOperations, un as validateAssetBucketName, ur as ResourceUpdateNotSupportedError, ut as WAFv2WebACLProvider, v as IMPLICIT_DELETE_DEPENDENCIES, vn as AssetManifestLoader, vr as withErrorHandling, vt as replayWarn, w as createPreDeleteFinalSnapshot, wn as resolveApp, wt as TEMPLATE_SOURCED_RULES, x as PRE_DELETE_SNAPSHOT_TYPES, xn as synthesisStatusMessage, xr as isThrottlingError, xt as requireConfigString, y as computeImplicitDeleteEdges, yn as getDockerImageBySourceHash, yr as isMarkedNonRetryable, yt as requireConfigArray, z as yellow, zn as PARTITION_TABLE, zt as DiffCalculator } from "./deploy-engine-DYdEWX-s.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-qMgJEhCM.js";
4
+ import { t as ASGProvider } from "./asg-provider-DJvcY8lj.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -77,6 +77,162 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
77
77
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
78
78
  import { WebSocketServer } from "ws";
79
79
 
80
+ //#region src/cli/commands/state-file-keys.ts
81
+ /**
82
+ * Shared helpers for enumerating and describing cdkd state-bucket keys.
83
+ *
84
+ * Extracted from `bootstrap-destroy.ts` (issue #1010) so that `cdkd gc`
85
+ * (issue #1012) reuses the exact same whole-bucket state-file discovery
86
+ * instead of duplicating it — the "scan the WHOLE bucket, not just the
87
+ * default `cdkd/` prefix" rule was a review blocker once already (PR #1018)
88
+ * and must not drift between the two commands.
89
+ */
90
+ /**
91
+ * Every cdkd state file ends with this suffix, regardless of the
92
+ * `--state-prefix` it was deployed under (`{prefix}/{stack}/{region}/state.json`,
93
+ * or legacy `{prefix}/{stack}/state.json`).
94
+ */
95
+ const STATE_FILE_SUFFIX = "/state.json";
96
+ /**
97
+ * The prefix `cdkd bootstrap` writes under, and the one `gc` /
98
+ * `bootstrap --destroy` configure their own state backend with.
99
+ *
100
+ * Shared here rather than spelled per command for the same reason this module
101
+ * exists at all: `bootstrap-destroy.ts` and `gc.ts` each held their own copy,
102
+ * and it is now also the `knownPrefix` argument to {@link describeStateKey},
103
+ * where a copy that drifted from the backend's actual prefix would silently
104
+ * send every key back to the shape heuristic instead of the exact depth rule.
105
+ *
106
+ * Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
107
+ * the whole-bucket listings deliberately do not scope to it.
108
+ */
109
+ const DEFAULT_STATE_PREFIX = "cdkd";
110
+ /**
111
+ * Segments left under a known prefix for the legacy `{prefix}/{stack}` layout.
112
+ *
113
+ * The twin of `S3StateBackend`'s `LEGACY_KEY_DEPTH`, which counts the
114
+ * `state.json` segment as well; this one counts what remains after the prefix
115
+ * and the suffix are stripped, so it is that value minus one. Fenced against it
116
+ * by a unit test rather than imported, because a value import from
117
+ * `s3-state-backend.js` breaks the suites that mock that module.
118
+ */
119
+ const LEGACY_SEGMENTS_UNDER_PREFIX = 1;
120
+ /**
121
+ * Every cdkd stack lock ends with this suffix — the lock lives next to the
122
+ * state file (`{prefix}/{stack}/{region}/lock.json`, or the legacy region-less
123
+ * `{prefix}/{stack}/lock.json` that `lock-manager.ts` still writes for a
124
+ * pre-v2 stack).
125
+ */
126
+ const LOCK_FILE_SUFFIX = "/lock.json";
127
+ /**
128
+ * `us-east-1` / `ap-northeast-1` / `us-gov-west-1` — a region-shaped segment.
129
+ *
130
+ * The first token is `[a-z]{2}` OR the literal `eusc` (issue #2001). Every
131
+ * region AWS ships has a two-letter first token except the European Sovereign
132
+ * Cloud partition's, which is four — `eusc-de-east-1` — and the old
133
+ * `^[a-z]{2}` rejected that whole partition. Enumerated from
134
+ * `aws-cdk-lib/region-info` and `@aws-sdk/util-endpoints`: 46 REAL regions,
135
+ * first tokens all two letters or `eusc`, none three. (A count of 53 includes
136
+ * the `aws-global` / `aws-iso-*-global` PSEUDO-regions, whose first token is
137
+ * `aws`; they carry no trailing `-<digits>`, so this pattern rejects them and
138
+ * so did the old one — no behavioural difference, but at 53 the "two letters
139
+ * or eusc" claim is false as stated.)
140
+ *
141
+ * **The exception is enumerated rather than widened, and that is the whole
142
+ * design.** The obvious fix — relaxing the bound to `{2,4}` — was written
143
+ * first and review measured what it cost: `api-prod-1`, `demo-app-1`,
144
+ * `dev-api-1`, `core-api-1`, `test-stack-2` all become region-shaped under it.
145
+ * Those are idiomatic STACK names, and in the legacy region-less layout a
146
+ * stack name sits exactly where a region would, so the pattern would report
147
+ * the key's PREFIX as the stack for each of them — the same wrong answer this
148
+ * issue is about, arriving from the other side and at a far higher rate. A
149
+ * length class cannot separate `eusc` from `demo`; only naming the partition
150
+ * can.
151
+ *
152
+ * The cost of enumerating is that a future partition with a new non-two-letter
153
+ * token needs this line updated. That is the same failure this issue fixed, it
154
+ * is a one-token change, and it fails in the recoverable direction (a region
155
+ * read as a stack name) rather than corrupting ordinary stack names.
156
+ *
157
+ * It stays SHAPE-based and deliberately does not reuse `isClientSafeRegion`
158
+ * from `intrinsic-function-resolver.ts`: that predicate is charset-based (its
159
+ * job is to keep a value inside a hostname label, so it accepts anything
160
+ * lowercase-alphanumeric) and would classify almost every stack name as a
161
+ * region. This one has the opposite job — telling a region segment apart from
162
+ * a stack name in the same position — so it must constrain the shape.
163
+ *
164
+ * A two-letter STACK name like `db-prod-1` is still misread in the legacy
165
+ * layout; that is pre-existing (`main` does the same) and is what the
166
+ * `knownPrefix` depth rule below repairs for callers that can supply one.
167
+ */
168
+ const REGION_SEGMENT = /^(?:[a-z]{2}|eusc)(-[a-z]+)+-\d+$/;
169
+ /**
170
+ * List every state file in the bucket — the WHOLE bucket, not just the
171
+ * default `cdkd/` prefix. Other commands accept `--state-prefix`, so live
172
+ * stack state may exist under ANY prefix in this bucket; scoping this
173
+ * listing to the default prefix would let reference scans and teardown
174
+ * guards silently miss those stacks and delete live data.
175
+ */
176
+ async function listAllStateKeys(stateBackend) {
177
+ return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(STATE_FILE_SUFFIX));
178
+ }
179
+ /**
180
+ * List every stack lock file in the bucket — same whole-bucket rule as
181
+ * {@link listAllStateKeys}. A lock under ANY prefix means a deploy /
182
+ * destroy may be in flight for that stack.
183
+ */
184
+ async function listAllLockKeys(stateBackend) {
185
+ return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(LOCK_FILE_SUFFIX));
186
+ }
187
+ /**
188
+ * `{prefix}/{stack}/{region}/state.json` → `stack (region)`; legacy
189
+ * `{prefix}/{stack}/state.json` → `stack`. Pass {@link LOCK_FILE_SUFFIX} to
190
+ * describe lock keys.
191
+ *
192
+ * The shape heuristic below cannot be made exact, and it errs in BOTH
193
+ * directions -- a region it fails to recognise is reported AS the stack name,
194
+ * while a region-shaped STACK name in the legacy layout makes it report the
195
+ * key's PREFIX as the stack. Neither is safe: `cdkd gc`'s corrupt-state abort
196
+ * turns whichever answer it gets into a `cdkd state show ...` recovery
197
+ * command, so a wrong one is handed to the user as a command to run.
198
+ *
199
+ * `knownPrefix` narrows it where a caller can: a key with exactly one segment
200
+ * under a prefix we know is the legacy `{stack}` layout by construction,
201
+ * whatever the stack is named, so no shape test is needed there at all. That
202
+ * repairs the pre-existing two-letter case (`cdkd/db-prod-1/state.json`, which
203
+ * `main` describes as `cdkd (db-prod-1)`).
204
+ *
205
+ * **It is a narrowing, not a safety net for the pattern above.** A first cut
206
+ * treated it as one — widening `REGION_SEGMENT` to `{2,4}` and relying on this
207
+ * rule to undo the damage — and review measured that the damage is repo-wide
208
+ * while this rule reaches only keys under the ONE prefix a caller names. The
209
+ * listing deliberately spans the whole bucket, so foreign and nested prefixes
210
+ * are in scope and unreachable from here; six shapes regressed against `main`
211
+ * that way. The pattern must be right on its own.
212
+ *
213
+ * **It deliberately does NOT claim the two-segment case.** A first cut did, on
214
+ * the reasoning that at that depth the last segment must be the region -- and
215
+ * review found `--state-prefix cdkd/team-a`, which nests inside the default
216
+ * prefix, so `cdkd/team-a/MyStack/state.json` is a LEGACY key two segments
217
+ * deep and would have been mis-split as `team-a (MyStack)` where `main` said
218
+ * `MyStack`. Depth cannot tell that apart from a real `{stack}/{region}`; only
219
+ * the shape of the tail can, so the heuristic keeps that case.
220
+ */
221
+ function describeStateKey(key, suffix = STATE_FILE_SUFFIX, knownPrefix) {
222
+ const body = key.slice(0, -suffix.length);
223
+ const prefix = knownPrefix?.replace(/\/+$/, "");
224
+ if (prefix !== void 0 && prefix !== "" && key.startsWith(`${prefix}/`)) {
225
+ const rest = body.slice(prefix.length + 1).split("/");
226
+ if (rest.length === LEGACY_SEGMENTS_UNDER_PREFIX && rest[0]) return rest[0];
227
+ }
228
+ const segments = body.split("/");
229
+ const last = segments[segments.length - 1] ?? key;
230
+ const secondLast = segments[segments.length - 2];
231
+ if (secondLast !== void 0 && secondLast !== "" && REGION_SEGMENT.test(last)) return `${secondLast} (${last})`;
232
+ return last;
233
+ }
234
+
235
+ //#endregion
80
236
  //#region src/cli/options.ts
81
237
  /**
82
238
  * Parse context key=value pairs from CLI arguments into a Record
@@ -152,7 +308,7 @@ const appOptions = [new Option("-a, --app <command>", "CDK app command (e.g., \"
152
308
  * --state-bucket is optional: falls back to CDKD_STATE_BUCKET env var,
153
309
  * then cdk.json context.cdkd.stateBucket
154
310
  */
155
- const stateOptions = [new Option("--state-bucket <bucket>", "S3 bucket for state storage. Falls back to CDKD_STATE_BUCKET env or cdk.json"), new Option("--state-prefix <prefix>", "S3 key prefix for state files").default("cdkd")];
311
+ const stateOptions = [new Option("--state-bucket <bucket>", "S3 bucket for state storage. Falls back to CDKD_STATE_BUCKET env or cdk.json"), new Option("--state-prefix <prefix>", "S3 key prefix for state files").default(DEFAULT_STATE_PREFIX)];
156
312
  /**
157
313
  * Stack options
158
314
  */
@@ -1025,91 +1181,9 @@ function regionNeedsReconciliation(effective) {
1025
1181
  return effective.region !== LEGACY_DEFAULT_REGION;
1026
1182
  }
1027
1183
 
1028
- //#endregion
1029
- //#region src/cli/commands/state-file-keys.ts
1030
- /**
1031
- * Shared helpers for enumerating and describing cdkd state-bucket keys.
1032
- *
1033
- * Extracted from `bootstrap-destroy.ts` (issue #1010) so that `cdkd gc`
1034
- * (issue #1012) reuses the exact same whole-bucket state-file discovery
1035
- * instead of duplicating it — the "scan the WHOLE bucket, not just the
1036
- * default `cdkd/` prefix" rule was a review blocker once already (PR #1018)
1037
- * and must not drift between the two commands.
1038
- */
1039
- /**
1040
- * Every cdkd state file ends with this suffix, regardless of the
1041
- * `--state-prefix` it was deployed under (`{prefix}/{stack}/{region}/state.json`,
1042
- * or legacy `{prefix}/{stack}/state.json`).
1043
- */
1044
- const STATE_FILE_SUFFIX = "/state.json";
1045
- /**
1046
- * Every cdkd stack lock ends with this suffix — the lock lives next to the
1047
- * state file (`{prefix}/{stack}/{region}/lock.json`).
1048
- */
1049
- const LOCK_FILE_SUFFIX = "/lock.json";
1050
- /** `us-east-1` / `ap-northeast-1` / `us-gov-west-1` — a region-shaped segment. */
1051
- const REGION_SEGMENT = /^[a-z]{2}(-[a-z]+)+-\d+$/;
1052
- /**
1053
- * List every state file in the bucket — the WHOLE bucket, not just the
1054
- * default `cdkd/` prefix. Other commands accept `--state-prefix`, so live
1055
- * stack state may exist under ANY prefix in this bucket; scoping this
1056
- * listing to the default prefix would let reference scans and teardown
1057
- * guards silently miss those stacks and delete live data.
1058
- */
1059
- async function listAllStateKeys(stateBackend) {
1060
- return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(STATE_FILE_SUFFIX));
1061
- }
1062
- /**
1063
- * List every stack lock file in the bucket — same whole-bucket rule as
1064
- * {@link listAllStateKeys}. A lock under ANY prefix means a deploy /
1065
- * destroy may be in flight for that stack.
1066
- */
1067
- async function listAllLockKeys(stateBackend) {
1068
- return (await stateBackend.listRawKeys("")).filter((k) => k.endsWith(LOCK_FILE_SUFFIX));
1069
- }
1070
- /**
1071
- * `{prefix}/{stack}/{region}/state.json` → `stack (region)`; legacy
1072
- * `{prefix}/{stack}/state.json` → `stack`. The prefix is arbitrary (custom
1073
- * `--state-prefix` values included), so the stack/region pair is derived
1074
- * from the key's TAIL: the last segment is treated as a region only when
1075
- * it is region-shaped. Pass {@link LOCK_FILE_SUFFIX} to describe lock keys.
1076
- */
1077
- function describeStateKey(key, suffix = STATE_FILE_SUFFIX) {
1078
- const segments = key.slice(0, -suffix.length).split("/");
1079
- const last = segments[segments.length - 1] ?? key;
1080
- const secondLast = segments[segments.length - 2];
1081
- if (secondLast !== void 0 && REGION_SEGMENT.test(last)) return `${secondLast} (${last})`;
1082
- return last;
1083
- }
1084
-
1085
1184
  //#endregion
1086
1185
  //#region src/cli/commands/bootstrap-destroy.ts
1087
1186
  /**
1088
- * `cdkd bootstrap --destroy` — teardown of cdkd-created account resources
1089
- * (issue #1010, the reverse of `cdkd bootstrap` / `ensureAssetStorage`).
1090
- *
1091
- * Scope per invocation is ONE region's asset storage (`--region`, same
1092
- * option the create side uses): empty + delete the asset bucket, force-
1093
- * delete the container-asset ECR repo, then delete the region's bootstrap
1094
- * marker LAST — the mirror image of the create side's marker-written-last
1095
- * ordering, so a crash mid-teardown leaves the region still consistently
1096
- * opted in (marker present → deploys keep hard-erroring at the missing
1097
- * resources with a re-bootstrap hint, never a silent legacy fallback).
1098
- *
1099
- * The asset bucket / repo NAMES are read from the region's bootstrap
1100
- * marker, NOT recomputed from the naming convention — the marker is the
1101
- * source of truth for names (design §4.1), which keeps this teardown
1102
- * compatible with custom asset-storage names (issue #1011).
1103
- *
1104
- * The state bucket is NOT deleted by default: it is the account's source
1105
- * of truth. `--include-state-bucket` opts in, and even then the deletion
1106
- * is refused while ANY stack state exists in the bucket or any OTHER
1107
- * region is still opted in to asset storage (deleting their markers would
1108
- * silently flip those regions back to legacy mode).
1109
- */
1110
- /** S3 key prefix for state files — same fixed value the create side uses. */
1111
- const STATE_PREFIX = "cdkd";
1112
- /**
1113
1187
  * Interactive confirmation for the teardown. Follows the repo's
1114
1188
  * destructive-prompt convention (see `recreate-confirm-prompt.ts`, the
1115
1189
  * same pattern family as `confirm-prompt.ts`): print the deletion plan as
@@ -1157,7 +1231,7 @@ async function scanStateReferences(stateBackend, names) {
1157
1231
  for (const key of stateKeys) {
1158
1232
  const body = await stateBackend.getRawObject(key);
1159
1233
  if (body === null) continue;
1160
- if (names.some((name) => body.includes(name))) referencing.push(describeStateKey(key));
1234
+ if (names.some((name) => body.includes(name))) referencing.push(describeStateKey(key, STATE_FILE_SUFFIX, DEFAULT_STATE_PREFIX));
1161
1235
  }
1162
1236
  return referencing;
1163
1237
  }
@@ -1332,7 +1406,7 @@ async function bootstrapDestroyCommand(options) {
1332
1406
  ...options.profile && { profile: options.profile }
1333
1407
  }), {
1334
1408
  bucket: bucketName,
1335
- prefix: STATE_PREFIX
1409
+ prefix: DEFAULT_STATE_PREFIX
1336
1410
  }, {
1337
1411
  region: effective.region,
1338
1412
  ...options.profile && { profile: options.profile }
@@ -1361,7 +1435,7 @@ async function bootstrapDestroyCommand(options) {
1361
1435
  });
1362
1436
  const stateBackend = new S3StateBackend(markerS3Client, {
1363
1437
  bucket: bucketName,
1364
- prefix: STATE_PREFIX
1438
+ prefix: DEFAULT_STATE_PREFIX
1365
1439
  }, {
1366
1440
  region,
1367
1441
  ...options.profile && { profile: options.profile }
@@ -1413,7 +1487,7 @@ async function bootstrapDestroyCommand(options) {
1413
1487
  if (options.includeStateBucket) {
1414
1488
  const stateKeys = await listAllStateKeys(stateBackend);
1415
1489
  if (stateKeys.length > 0) {
1416
- const listing = stateKeys.map((k) => ` - ${describeStateKey(k)} [${k}]`).join("\n");
1490
+ const listing = stateKeys.map((k) => ` - ${describeStateKey(k, STATE_FILE_SUFFIX, DEFAULT_STATE_PREFIX)} [${k}]`).join("\n");
1417
1491
  throw new CdkdError(`Refusing to delete state bucket '${bucketName}': ${stateKeys.length} stack(s) still have state in it:\n${listing}\nDestroy every stack ('cdkd destroy' / 'cdkd state destroy') before deleting the state bucket.`, "STATE_BUCKET_NOT_EMPTY");
1418
1492
  }
1419
1493
  if (markerSiblings.otherRegions.length > 0) throw new CdkdError(`Refusing to delete state bucket '${bucketName}': region(s) ${markerSiblings.otherRegions.join(", ")} are still opted in to cdkd asset storage (their bootstrap markers live in this bucket). Run 'cdkd bootstrap --destroy --region <r>' for each first.`, "STATE_BUCKET_HOLDS_MARKERS");
@@ -1509,7 +1583,7 @@ async function bootstrapCommand(options) {
1509
1583
  ...options.profile && { profile: options.profile }
1510
1584
  }), {
1511
1585
  bucket: bucketName,
1512
- prefix: "cdkd"
1586
+ prefix: DEFAULT_STATE_PREFIX
1513
1587
  }, {
1514
1588
  region: effective.region,
1515
1589
  ...options.profile && { profile: options.profile }
@@ -1610,7 +1684,7 @@ async function bootstrapCommand(options) {
1610
1684
  });
1611
1685
  const stateBackend = new S3StateBackend(markerS3Client, {
1612
1686
  bucket: bucketName,
1613
- prefix: "cdkd"
1687
+ prefix: DEFAULT_STATE_PREFIX
1614
1688
  }, {
1615
1689
  region,
1616
1690
  ...options.profile && { profile: options.profile }
@@ -73031,7 +73105,7 @@ async function scanReferencedAssets(stateBackend, marker, logger) {
73031
73105
  try {
73032
73106
  parsed = JSON.parse(body);
73033
73107
  } catch (error) {
73034
- const described = describeStateKey(key);
73108
+ const described = describeStateKey(key, STATE_FILE_SUFFIX, DEFAULT_STATE_PREFIX);
73035
73109
  const regionMatch = /^(\S+) \((\S+)\)$/.exec(described);
73036
73110
  throw new CdkdError(`State file '${key}' is not valid JSON — aborting: gc must know every referenced asset before deleting anything, and this file's references are unreadable. Repair or remove the corrupt state file ('${regionMatch ? `cdkd state show ${regionMatch[1]} --stack-region ${regionMatch[2]}` : `cdkd state show ${described}`}' to inspect), then re-run.`, "GC_STATE_UNREADABLE", error);
73037
73111
  }
@@ -73236,7 +73310,7 @@ async function gcCommand(options) {
73236
73310
  });
73237
73311
  const stateBackend = new S3StateBackend(markerS3Client, {
73238
73312
  bucket: bucketName,
73239
- prefix: "cdkd"
73313
+ prefix: DEFAULT_STATE_PREFIX
73240
73314
  }, {
73241
73315
  region: effective.region,
73242
73316
  ...options.profile && { profile: options.profile }
@@ -73282,7 +73356,7 @@ async function gcCommand(options) {
73282
73356
  const marker = parseBootstrapMarker(markerBody, resolvedMarkerKey);
73283
73357
  const lockKeys = await listAllLockKeys(stateBackend);
73284
73358
  if (lockKeys.length > 0) {
73285
- const listing = lockKeys.map((k) => ` - ${describeStateKey(k, LOCK_FILE_SUFFIX)} [${k}]`).join("\n");
73359
+ const listing = lockKeys.map((k) => ` - ${describeStateKey(k, LOCK_FILE_SUFFIX, DEFAULT_STATE_PREFIX)} [${k}]`).join("\n");
73286
73360
  throw new CdkdError(`Refusing to gc while ${lockKeys.length} stack(s) hold an active lock (a deploy in flight may have published assets whose state write has not landed yet):\n${listing}\nWait for the operation(s) to finish — or clear a stale lock with 'cdkd force-unlock <stack>' — then re-run 'cdkd gc'.`, "GC_LOCKED");
73287
73361
  }
73288
73362
  const refs = await scanReferencedAssets(stateBackend, marker, logger);
@@ -85084,7 +85158,7 @@ async function promptConfirm(message) {
85084
85158
  * callers get the standard exit-code-2 routing.
85085
85159
  */
85086
85160
  function createMigrateCommand() {
85087
- return new Command("migrate").description("Adopt a plain (non-CDK) CloudFormation stack into a cdkd-managed CDK app. Generates new CDK code via upstream `cdk migrate`, builds a logical-ID mapping between the source CFn template and the synth template, writes cdkd state, and (optionally) retires the source CFn stack. AWS resources are never modified.").argument("[stack]", "Source CFn stack name. Alias for --from-cfn-stack.").addOption(new Option("--from-cfn-stack <name>", "Source CloudFormation stack name to adopt. Required (or pass positionally).")).addOption(new Option("--output-dir <dir>", "Directory to write the generated CDK app to. Defaults to <cwd>/<CfnStackName>.")).addOption(new Option("--language <choice>", "Generated code language. v1: typescript only.").choices(["typescript"]).default("typescript")).addOption(new Option("--region <region>", "AWS region. Defaults to AWS_REGION env / profile.")).addOption(new Option("--account <id>", "AWS account ID. Auto-detected via STS when omitted.")).addOption(new Option("--retire-cfn-stack", "After cdkd state is written, inject DeletionPolicy=Retain on every resource in the source CFn stack and DeleteStack. AWS resources stay; the CFn stack record is gone. Off by default.").default(false)).addOption(new Option("--filter <key=value>", "Pass-through to `cdk migrate --filter` for resource subsetting. Repeatable.").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--skip-install", "Skip `npm install` after codegen.").default(false)).addOption(new Option("--skip-synth", "Skip `cdk synth` (does NOT write cdkd state). Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("--dry-run", "Print the import plan without writing state or retiring the CFn stack. Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("-y, --yes", "Auto-confirm the import + retirement prompts.").default(false)).addOption(new Option("--cdk-bin <path>", "Override the `cdk` binary path.")).addOption(new Option("--resource-mapping <file>", `Path to a JSON file of {sourceLogicalId: synthLogicalId} overrides. Same shape as the auto-written ${RESOURCE_MAPPING_FILENAME}.`)).addOption(new Option("--state-bucket <name>", "cdkd state bucket. Defaults to cdkd-state-<accountId>.")).addOption(new Option("--state-prefix <prefix>", "cdkd state prefix inside the bucket.").default("cdkd")).addOption(new Option("--profile <name>", "AWS profile name.")).addOption(new Option("--role-arn <arn>", "IAM role to assume before any AWS call.")).addOption(new Option("--verbose", "Enable debug-level logging.").default(false)).action(withErrorHandling(migrateCommandAction));
85161
+ return new Command("migrate").description("Adopt a plain (non-CDK) CloudFormation stack into a cdkd-managed CDK app. Generates new CDK code via upstream `cdk migrate`, builds a logical-ID mapping between the source CFn template and the synth template, writes cdkd state, and (optionally) retires the source CFn stack. AWS resources are never modified.").argument("[stack]", "Source CFn stack name. Alias for --from-cfn-stack.").addOption(new Option("--from-cfn-stack <name>", "Source CloudFormation stack name to adopt. Required (or pass positionally).")).addOption(new Option("--output-dir <dir>", "Directory to write the generated CDK app to. Defaults to <cwd>/<CfnStackName>.")).addOption(new Option("--language <choice>", "Generated code language. v1: typescript only.").choices(["typescript"]).default("typescript")).addOption(new Option("--region <region>", "AWS region. Defaults to AWS_REGION env / profile.")).addOption(new Option("--account <id>", "AWS account ID. Auto-detected via STS when omitted.")).addOption(new Option("--retire-cfn-stack", "After cdkd state is written, inject DeletionPolicy=Retain on every resource in the source CFn stack and DeleteStack. AWS resources stay; the CFn stack record is gone. Off by default.").default(false)).addOption(new Option("--filter <key=value>", "Pass-through to `cdk migrate --filter` for resource subsetting. Repeatable.").argParser((value, previous) => [...previous ?? [], value]).default([])).addOption(new Option("--skip-install", "Skip `npm install` after codegen.").default(false)).addOption(new Option("--skip-synth", "Skip `cdk synth` (does NOT write cdkd state). Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("--dry-run", "Print the import plan without writing state or retiring the CFn stack. Mutually exclusive with --retire-cfn-stack.").default(false)).addOption(new Option("-y, --yes", "Auto-confirm the import + retirement prompts.").default(false)).addOption(new Option("--cdk-bin <path>", "Override the `cdk` binary path.")).addOption(new Option("--resource-mapping <file>", `Path to a JSON file of {sourceLogicalId: synthLogicalId} overrides. Same shape as the auto-written ${RESOURCE_MAPPING_FILENAME}.`)).addOption(new Option("--state-bucket <name>", "cdkd state bucket. Defaults to cdkd-state-<accountId>.")).addOption(new Option("--state-prefix <prefix>", "cdkd state prefix inside the bucket.").default(DEFAULT_STATE_PREFIX)).addOption(new Option("--profile <name>", "AWS profile name.")).addOption(new Option("--role-arn <arn>", "IAM role to assume before any AWS call.")).addOption(new Option("--verbose", "Enable debug-level logging.").default(false)).action(withErrorHandling(migrateCommandAction));
85088
85162
  }
85089
85163
 
85090
85164
  //#endregion
@@ -85107,7 +85181,7 @@ function createMigrateCommand() {
85107
85181
  */
85108
85182
  function buildProgram() {
85109
85183
  const program = new Command();
85110
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.30");
85184
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.31");
85111
85185
  program.hook("preAction", (_thisCommand, actionCommand) => {
85112
85186
  const { profile } = actionCommand.optsWithGlobals();
85113
85187
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;