@go-to-k/cdkd 0.284.37 → 0.284.39

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 startInterruptWatch, $t as buildAssetRedirectMap, A as renderStatefulReason, An as resolveStateBucketWithDefaultAndSource, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as PARTITION_TABLE, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as getDefaultStateBucketName, Cr as isRetryableTransientError, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as resolveCaptureObservedState, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as resolveAutoAssetStorage, Et as errorCauseChain, F as green, Fn as CFN_TEMPLATE_URL_LIMIT, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as derivePartitionAndUrlSuffix, Ht as describeTypeWithThrottleRetry, I as red, In as MIGRATE_TMP_PREFIX, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jn as getAwsClients, Jt as rebuildClientForBucketRegion, K as clearOnUpdateRemoval, Kn as resolveBucketRegion, Kt as LockManager, L as yellow, Ln as findLargeInlineResources, Lt as s3BucketRegionalDomainName, M as bold, Mn as stateBucketExistenceConfirmed, Mt as classifyReplaySecretRegion, N as cyan, Nn as warnDeprecatedNoPrefixCliFlag, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveSkipPrefix, Ot as maskSecretsInError, P as gray, Pn as CFN_TEMPLATE_BODY_LIMIT, Pt as s3BucketArn, Q as isInterruptedWaitError, Qn as CdkdError, Qt as WorkGraph, R as collectDeclaredOutputNames, Rn as uploadCfnTemplate, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as synthesisStatusMessage, Sr as isMarkedNonRetryable, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as resolveApp, Tr as markNonRetryable, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as AssemblyReader, Ut as withRetry, V as isExportAliasCollision, Vn as canonicalizeRegion, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as processStackMessages, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as setAwsClients, Xt as AssetPublisher, Y as findSilentDropProperties, Yn as resetAwsClients, Yt as shouldRetainResource, Z as endCommandInterruptScope, Zt as stringifyValue, _ as computeImplicitDeleteEdges, _n as runDockerForeground, _t as replayWarn, a as DeploymentEventsStore, an as AssetModeResolver, ar as LocalMigrateError, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as getDockerImageBySourceHash, br as normalizeAwsError, bt as requireConfigString, c as replayFailedOperations, cn as getBootstrapMarkerKey, cr as MissingCdkCliError, ct as refStateLookupFromResource, d as updatePartialReason, dn as validateAssetBucketName, dr as ProvisioningError, dt as resolveExplicitPhysicalId, en as createAssetRedirectResolver, er as CrossAccountSecretRefusalError, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateContainerRepoName, fr as ResourceTimeoutError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as getDockerCmd, gt as readConfigString, h as maskingRetryLogger, hn as formatDockerLoginError, hr as StackTerminationProtectionError, ht as configStringRefusal, i as DeploymentEventsReader, in as stripControlChars, ir as LocalInvokeBuildError$1, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveUseCdkBootstrapAssets, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveStateBucketWithDefault, kt as maskSecretsInText, l as replayRollback, ln as parseBootstrapMarker, lr as NestedStackChildDirectDestroyError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDockerImage, mr as StackHasActiveImportsError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as rewriteTemplateAssetReferences, nr as DeployCancelledError, nt as disableInstanceApiTermination, o as planFailedOps, on as BOOTSTRAP_MARKER_PREFIX, or as LocalStartServiceError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as buildDenyExternalAccessPolicy, pr as ResourceUpdateNotSupportedError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as AwsClients, qt as S3StateBackend, r as DeployEngine, rn as escapeRegExp, rr as DynamicReferenceRegionAmbiguousError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as ensureAssetStorage, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as loadPublishableAssetManifest, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as readBootstrapMarkerBody, ur as PartialFailureError, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as runDockerStreaming, vt as requireConfigArray, w as refusesFinalSnapshot, wn as getLegacyStateBucketName, wr as isThrottlingError, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as Synthesizer, xr as withErrorHandling, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as AssetManifestLoader, yt as requireConfigObject, z as collectPublishedOutputNames, zn as expectedOwnerParam, zt as applyRoleArnIfSet } from "./deploy-engine-C62UGXid.js";
2
+ import { $ as startInterruptWatch, $n as CdkdError, $t as WorkGraph, A as renderStatefulReason, An as resolveStateBucketWithDefault, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as expectedOwnerParam, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as synthesisStatusMessage, Cr as isMarkedNonRetryable, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as resolveAutoAssetStorage, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as resolveApp, Er as markNonRetryable, Et as errorCauseChain, F as green, Fn as CFN_TEMPLATE_BODY_LIMIT, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as processStackMessages, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as canonicalizeRegion, Ht as describeTypeWithThrottleRetry, I as red, In as CFN_TEMPLATE_URL_LIMIT, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jn as AwsClients, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kt as LockManager, L as yellow, Ln as MIGRATE_TMP_PREFIX, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveUseCdkBootstrapAssets, Mt as classifyReplaySecretRegion, N as cyan, Nn as stateBucketExistenceConfirmed, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveCaptureObservedState, Ot as maskSecretsInError, P as gray, Pn as warnDeprecatedNoPrefixCliFlag, Pt as s3BucketArn, Q as isInterruptedWaitError, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as findLargeInlineResources, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as Synthesizer, Sr as withErrorHandling, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as getLegacyStateBucketName, Tr as isThrottlingError, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as derivePartitionAndUrlSuffix, Ut as withRetry, V as isExportAliasCollision, Vn as PARTITION_TABLE, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as AssemblyReader, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as resetAwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as getAwsClients, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as setAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as getDockerCmd, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as LocalInvokeBuildError$1, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as AssetManifestLoader, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as PartialFailureError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as ProvisioningError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as formatDockerLoginError, gr as StackTerminationProtectionError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as StackHasActiveImportsError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, ir as DynamicReferenceRegionAmbiguousError, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveStateBucketWithDefaultAndSource, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveSkipPrefix, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as MissingCdkCliError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ResourceUpdateNotSupportedError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as LocalMigrateError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as ResourceTimeoutError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as resolveBucketRegion, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as DeployCancelledError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalStartServiceError, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CrossAccountSecretRefusalError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ur as NestedStackChildDirectDestroyError, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as runDockerForeground, vt as requireConfigArray, w as refusesFinalSnapshot, wn as getDefaultStateBucketName, wr as isRetryableTransientError, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as getDockerImageBySourceHash, xr as normalizeAwsError, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as runDockerStreaming, yt as requireConfigObject, z as collectPublishedOutputNames, zn as uploadCfnTemplate, zt as applyRoleArnIfSet } from "./deploy-engine-rKBFVifY.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-BbWubpjP.js";
4
+ import { t as ASGProvider } from "./asg-provider-Db6lnsaA.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,23 @@ 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/state/state-prefix.ts
81
+ /**
82
+ * The default S3 key prefix for cdkd state.
83
+ *
84
+ * Homed in the STATE layer rather than in `src/cli/commands/state-file-keys.ts`
85
+ * (which re-exports it, so its four existing importers are unchanged) because
86
+ * `src/state/lock-contention-message.ts` needs it to decide whether a recovery
87
+ * hint should spell `--state-prefix` at all, and a `src/state/**` module
88
+ * importing from `src/cli/commands/**` inverts the layering — the CLI sits
89
+ * ABOVE the state layer in the 7-layer architecture, not below it.
90
+ *
91
+ * Note this is only the DEFAULT. Other commands accept `--state-prefix`, so
92
+ * whole-bucket listings deliberately do not scope to it.
93
+ */
94
+ const DEFAULT_STATE_PREFIX = "cdkd";
95
+
96
+ //#endregion
80
97
  //#region src/cli/commands/state-file-keys.ts
81
98
  /**
82
99
  * Shared helpers for enumerating and describing cdkd state-bucket keys.
@@ -94,20 +111,6 @@ import { WebSocketServer } from "ws";
94
111
  */
95
112
  const STATE_FILE_SUFFIX = "/state.json";
96
113
  /**
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
114
  * Segments left under a known prefix for the legacy `{prefix}/{stack}` layout.
112
115
  *
113
116
  * The twin of `S3StateBackend`'s `LEGACY_KEY_DEPTH`, which counts the
@@ -58162,6 +58165,104 @@ var ECRProvider = class {
58162
58165
  }
58163
58166
  };
58164
58167
 
58168
+ //#endregion
58169
+ //#region src/state/lock-contention-message.ts
58170
+ /**
58171
+ * The message every fail-fast lock-contention site raises.
58172
+ *
58173
+ * Issue #2161 made six commands throw on `acquireLock` returning `false`
58174
+ * instead of running under a foreign lock. Issue #2170 is the follow-up: the
58175
+ * message those sites raise asked the user to decide "is another process
58176
+ * active?" while printing none of the evidence that would answer it, and the
58177
+ * recovery command it suggested could resolve against the wrong AWS account.
58178
+ *
58179
+ * Both matter more than they look, because of WHICH locks reach this message.
58180
+ * `LockManager.acquireLock` reaps an EXPIRED foreign lock and retries, so a
58181
+ * `false` return means the lock is LIVE — in practice a running `cdkd deploy`.
58182
+ * The user who follows a bare `cdkd force-unlock <stack>` suggestion therefore
58183
+ * deletes a live owner's lock and reproduces issue #2161's harm by hand, which
58184
+ * is the outcome #2161 exists to prevent.
58185
+ *
58186
+ * Centralising the text here also settles the third finding: the nine sites
58187
+ * had drifted to three spellings (`for stack` / `for nested stack` /
58188
+ * `for nested-stack child`), so a user grepping CI logs for one of them found
58189
+ * two of three. `subject` now varies only the noun.
58190
+ */
58191
+ /**
58192
+ * Stand-in for a value with nothing renderable left after sanitization. Named
58193
+ * rather than inlined so the message and the command-suppression branch cannot
58194
+ * disagree about what "unrenderable" looks like.
58195
+ */
58196
+ const UNRENDERABLE = "<unrenderable>";
58197
+ /** Render `expiresIn` without implying more precision than a clock skew allows. */
58198
+ function formatRemaining(ms) {
58199
+ if (!Number.isFinite(ms)) return "at an unknown time";
58200
+ if (ms <= 0) return "already expired";
58201
+ const minutes = Math.round(ms / 6e4);
58202
+ if (minutes < 1) return "in under a minute";
58203
+ return `in ~${minutes}m`;
58204
+ }
58205
+ function shellQuote(value) {
58206
+ return /^[A-Za-z0-9._/@:+-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
58207
+ }
58208
+ /**
58209
+ * The `cdkd force-unlock ...` line, carrying every flag that decides which
58210
+ * lock object it resolves to.
58211
+ */
58212
+ function buildForceUnlockCommand(stackName, region, recovery) {
58213
+ const safeStack = displaySafe(stackName, { asciiOnly: true });
58214
+ const stackIsExact = safeStack === stackName;
58215
+ const safeRegion = region === void 0 ? void 0 : displaySafe(region, { asciiOnly: true });
58216
+ if (!safeStack || safeRegion === "" || !stackIsExact || !(region === void 0 || safeRegion === region)) return "";
58217
+ const parts = [safeRegion === void 0 ? `cdkd force-unlock ${shellQuote(safeStack)}` : `cdkd force-unlock ${shellQuote(safeStack)} --stack-region ${shellQuote(safeRegion)}`];
58218
+ if (recovery?.profile) parts.push(`--profile ${shellQuote(recovery.profile)}`);
58219
+ if (recovery?.stateBucket) parts.push(`--state-bucket ${shellQuote(recovery.stateBucket)}`);
58220
+ if (recovery?.statePrefix && recovery.statePrefix !== "cdkd") parts.push(`--state-prefix ${shellQuote(recovery.statePrefix)}`);
58221
+ return parts.join(" ");
58222
+ }
58223
+ /**
58224
+ * The force-quit banner's recovery sentence.
58225
+ *
58226
+ * Exported so the two `destroy-runner.ts` banners do not each decide what to
58227
+ * say when {@link buildForceUnlockCommand} suppresses — a banner ending in a
58228
+ * bare `run: ` is the shape the review found, and two copies of the branch is
58229
+ * how the next one drifts. Returns a leading-space clause so the caller can
58230
+ * concatenate it unconditionally.
58231
+ */
58232
+ function forceQuitRecoveryClause(stackName, region, recovery) {
58233
+ const command = buildForceUnlockCommand(stackName, region, recovery);
58234
+ return command ? ` If the next run reports a lock, run: ${command}` : " Inspect the lock object directly: the name or region recorded for this stack cannot be reproduced safely on a command line, so any command shown here would address a different lock.";
58235
+ }
58236
+ /**
58237
+ * Build the contention message, reading the holder's identity best-effort.
58238
+ *
58239
+ * The `getLockInfo` read is one GetObject and is deliberately NOT allowed to
58240
+ * fail the command: the caller is already on its way to throwing, and turning
58241
+ * a contention refusal into an S3 error would lose the reason. A failed or
58242
+ * absent read degrades to the evidence-free wording rather than to a crash.
58243
+ */
58244
+ async function buildLockContentionMessage(args) {
58245
+ const { lockManager, stackName, region, subject = "stack", recovery, heldClause, suffix } = args;
58246
+ let held = heldClause ?? "another cdkd process holds it";
58247
+ let sawHolder = false;
58248
+ try {
58249
+ const info = await lockManager.getLockInfo(stackName, region);
58250
+ if (info) {
58251
+ const operation = info.operation ? `, operation: ${displaySafe(info.operation)}` : "";
58252
+ const expires = formatRemaining(info.expiresAt - Date.now());
58253
+ const owner = displaySafe(info.owner);
58254
+ const holder = owner ? `held by ${owner}${operation}` : `held by an unnamed holder`;
58255
+ held = `${heldClause ? `${heldClause} — ` : ""}${holder}, expires ${expires}`;
58256
+ if (owner) sawHolder = true;
58257
+ }
58258
+ } catch {}
58259
+ const advice = sawHolder ? `That process is still running — wait for it to finish. Only if you are certain it is gone` : `Wait for it to finish, or if you are certain no other process is active`;
58260
+ const recoveryCommand = buildForceUnlockCommand(stackName, region, recovery);
58261
+ const head = `Could not acquire lock for ${subject} '${displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>"}' (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"}) — ${held}.` + (suffix ? ` ${suffix}` : "");
58262
+ if (!recoveryCommand) return `${head} ${advice}. No recovery command can be shown: the name or region recorded for this lock contains characters that cannot be reproduced safely on a command line, so any command shown here would address a different lock — inspect the lock object directly.`;
58263
+ return `${head} ${advice}, run: ${recoveryCommand}`;
58264
+ }
58265
+
58165
58266
  //#endregion
58166
58267
  //#region src/cli/commands/destroy-runner.ts
58167
58268
  /**
@@ -58298,9 +58399,64 @@ async function runDestroyForStack(stackName, state, ctx) {
58298
58399
  const regionForState = state.region ?? ctx.baseRegion;
58299
58400
  if (resourceCount === 0) {
58300
58401
  logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
58301
- await ctx.stateBackend.deleteState(stackName, regionForState);
58302
- logger.info(`${green("✓")} State deleted`);
58402
+ let emptyLockHeld = false;
58403
+ let emptyInterrupted = false;
58404
+ const emptySigintHandler = () => {
58405
+ if (emptyInterrupted) {
58406
+ if (emptyLockHeld) {
58407
+ ctx.lockManager.releaseLock(stackName, regionForState).catch(() => {});
58408
+ process.stderr.write(`\nForce-quit: stack lock may not be released.${forceQuitRecoveryClause(stackName, regionForState, {
58409
+ profile: ctx.profile,
58410
+ stateBucket: ctx.stateBucket,
58411
+ statePrefix: ctx.statePrefix
58412
+ })}\n`);
58413
+ }
58414
+ process.exit(130);
58415
+ }
58416
+ emptyInterrupted = true;
58417
+ try {
58418
+ process.stderr.write(`\nInterrupt received - finishing the state cleanup for ${stackName} (press Ctrl-C again to force-quit)\n`);
58419
+ } catch {}
58420
+ };
58421
+ process.setMaxListeners(Math.max(process.getMaxListeners(), 100));
58422
+ process.on("SIGINT", emptySigintHandler);
58423
+ let emptyAcquired;
58424
+ try {
58425
+ emptyAcquired = await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
58426
+ } catch (acquireErr) {
58427
+ process.removeListener("SIGINT", emptySigintHandler);
58428
+ throw acquireErr;
58429
+ }
58430
+ if (!emptyAcquired) {
58431
+ process.removeListener("SIGINT", emptySigintHandler);
58432
+ throw new Error(await buildLockContentionMessage({
58433
+ lockManager: ctx.lockManager,
58434
+ stackName,
58435
+ region: regionForState,
58436
+ recovery: {
58437
+ profile: ctx.profile,
58438
+ stateBucket: ctx.stateBucket,
58439
+ statePrefix: ctx.statePrefix
58440
+ }
58441
+ }));
58442
+ }
58443
+ emptyLockHeld = true;
58444
+ try {
58445
+ const recheck = await ctx.stateBackend.getState(stackName, regionForState);
58446
+ if (!(!recheck || Object.keys(recheck.state.resources).length === 0)) throw new Error(`Stack '${stackName}' (${regionForState}) was empty when this run started but now has ${Object.keys(recheck.state.resources).length} resource(s) — another cdkd process deployed into it. Re-run the destroy to act on the current state.`);
58447
+ await ctx.stateBackend.deleteState(stackName, regionForState);
58448
+ logger.info(`${green("✓")} State deleted`);
58449
+ } finally {
58450
+ try {
58451
+ await ctx.lockManager.releaseLock(stackName, regionForState);
58452
+ } catch (releaseErr) {
58453
+ logger.warn(`Failed to release lock after empty-state cleanup: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
58454
+ } finally {
58455
+ process.removeListener("SIGINT", emptySigintHandler);
58456
+ }
58457
+ }
58303
58458
  result.skippedEmpty = true;
58459
+ result.interrupted ||= emptyInterrupted;
58304
58460
  return result;
58305
58461
  }
58306
58462
  const needsStrongRefCheck = !!(state.outputs && Object.keys(state.outputs).length > 0);
@@ -58335,19 +58491,40 @@ async function runDestroyForStack(stackName, state, ctx) {
58335
58491
  const stackRegion = state.region;
58336
58492
  let destroyProviderRegistry = ctx.providerRegistry;
58337
58493
  let destroyAwsClients;
58494
+ let regionSwitched = false;
58495
+ const restoreBaseRegionAndClients = () => {
58496
+ if (!regionSwitched) return;
58497
+ regionSwitched = false;
58498
+ const switchedClients = destroyAwsClients;
58499
+ destroyAwsClients = void 0;
58500
+ process.env["AWS_REGION"] = ctx.baseRegion;
58501
+ process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
58502
+ setAwsClients(ctx.baseAwsClients);
58503
+ try {
58504
+ switchedClients?.destroy();
58505
+ } catch (destroyError) {
58506
+ logger.debug(`Failed to destroy region-scoped AWS clients: ${destroyError instanceof Error ? destroyError.message : String(destroyError)}`);
58507
+ }
58508
+ };
58338
58509
  if (stackRegion && stackRegion !== ctx.baseRegion) {
58339
58510
  logger.info(`Stack region: ${stackRegion}`);
58340
58511
  process.env["AWS_REGION"] = stackRegion;
58341
58512
  process.env["AWS_DEFAULT_REGION"] = stackRegion;
58342
- destroyAwsClients = new AwsClients({
58343
- region: stackRegion,
58344
- ...ctx.profile && { profile: ctx.profile }
58345
- });
58346
- setAwsClients(destroyAwsClients);
58347
- destroyProviderRegistry = new ProviderRegistry();
58348
- registerAllProviders(destroyProviderRegistry);
58349
- destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
58350
- if (ctx.allowUnsupportedTypes?.length) destroyProviderRegistry.allowUnsupportedTypes(ctx.allowUnsupportedTypes);
58513
+ regionSwitched = true;
58514
+ try {
58515
+ destroyAwsClients = new AwsClients({
58516
+ region: stackRegion,
58517
+ ...ctx.profile && { profile: ctx.profile }
58518
+ });
58519
+ setAwsClients(destroyAwsClients);
58520
+ destroyProviderRegistry = new ProviderRegistry();
58521
+ registerAllProviders(destroyProviderRegistry);
58522
+ destroyProviderRegistry.setCustomResourceResponseBucket(ctx.stateBucket);
58523
+ if (ctx.allowUnsupportedTypes?.length) destroyProviderRegistry.allowUnsupportedTypes(ctx.allowUnsupportedTypes);
58524
+ } catch (setupError) {
58525
+ restoreBaseRegionAndClients();
58526
+ throw setupError;
58527
+ }
58351
58528
  }
58352
58529
  const renderer = getLiveRenderer();
58353
58530
  let draining = false;
@@ -58356,7 +58533,11 @@ async function runDestroyForStack(stackName, state, ctx) {
58356
58533
  if (draining) {
58357
58534
  if (lockHeld) {
58358
58535
  ctx.lockManager.releaseLock(stackName, regionForState).catch(() => {});
58359
- process.stderr.write(`\nForce-quit: stack lock may not be released. If the next run reports a lock, run: cdkd force-unlock ${stackName}\n`);
58536
+ process.stderr.write(`\nForce-quit: stack lock may not be released.${forceQuitRecoveryClause(stackName, regionForState, {
58537
+ profile: ctx.profile,
58538
+ stateBucket: ctx.stateBucket,
58539
+ statePrefix: ctx.statePrefix
58540
+ })}\n`);
58360
58541
  }
58361
58542
  process.exit(130);
58362
58543
  }
@@ -58367,10 +58548,20 @@ async function runDestroyForStack(stackName, state, ctx) {
58367
58548
  };
58368
58549
  process.setMaxListeners(Math.max(process.getMaxListeners(), 100));
58369
58550
  process.on("SIGINT", sigintHandler);
58370
- logger.info(`\nAcquiring lock for stack ${stackName}...`);
58371
58551
  try {
58372
- await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy");
58552
+ logger.info(`\nAcquiring lock for stack ${stackName}...`);
58553
+ if (!await ctx.lockManager.acquireLock(stackName, regionForState, void 0, "destroy")) throw new Error(await buildLockContentionMessage({
58554
+ lockManager: ctx.lockManager,
58555
+ stackName,
58556
+ region: regionForState,
58557
+ recovery: {
58558
+ profile: ctx.profile,
58559
+ stateBucket: ctx.stateBucket,
58560
+ statePrefix: ctx.statePrefix
58561
+ }
58562
+ }));
58373
58563
  } catch (error) {
58564
+ restoreBaseRegionAndClients();
58374
58565
  process.removeListener("SIGINT", sigintHandler);
58375
58566
  throw error;
58376
58567
  }
@@ -58382,6 +58573,7 @@ async function runDestroyForStack(stackName, state, ctx) {
58382
58573
  } catch (releaseErr) {
58383
58574
  logger.warn(`Failed to release lock after strong-ref refusal/failure: ${releaseErr instanceof Error ? releaseErr.message : String(releaseErr)}`);
58384
58575
  }
58576
+ restoreBaseRegionAndClients();
58385
58577
  process.removeListener("SIGINT", sigintHandler);
58386
58578
  };
58387
58579
  let consumers;
@@ -58422,8 +58614,8 @@ async function runDestroyForStack(stackName, state, ctx) {
58422
58614
  }
58423
58615
  });
58424
58616
  };
58425
- renderer.start();
58426
58617
  try {
58618
+ renderer.start();
58427
58619
  logger.info("Building dependency graph...");
58428
58620
  const template = {
58429
58621
  AWSTemplateFormatVersion: "2010-09-09",
@@ -58693,14 +58885,9 @@ async function runDestroyForStack(stackName, state, ctx) {
58693
58885
  await ctx.lockManager.releaseLock(stackName, regionForState);
58694
58886
  } finally {
58695
58887
  process.removeListener("SIGINT", sigintHandler);
58888
+ restoreBaseRegionAndClients();
58696
58889
  }
58697
58890
  result.interrupted ||= draining;
58698
- if (destroyAwsClients) {
58699
- destroyAwsClients.destroy();
58700
- process.env["AWS_REGION"] = ctx.baseRegion;
58701
- process.env["AWS_DEFAULT_REGION"] = ctx.baseRegion;
58702
- setAwsClients(ctx.baseAwsClients);
58703
- }
58704
58891
  }
58705
58892
  return result;
58706
58893
  }
@@ -58966,6 +59153,7 @@ var NestedStackProvider = class {
58966
59153
  skipConfirmation: true,
58967
59154
  ...ctx.exportIndexStore && { exportIndexStore: ctx.exportIndexStore },
58968
59155
  ...ctx.destroyOptions?.profile && { profile: ctx.destroyOptions.profile },
59156
+ ...ctx.destroyOptions?.statePrefix !== void 0 && { statePrefix: ctx.destroyOptions.statePrefix },
58969
59157
  ...deleteContext?.removeProtection === true && { removeProtection: true },
58970
59158
  ...(ctx.destroyOptions?.skipFinalSnapshot === true || ctx.options?.skipFinalSnapshot === true) && { skipFinalSnapshot: true },
58971
59159
  ...ctx.destroyOptions?.resourceWarnAfterMs !== void 0 && { resourceWarnAfterMs: ctx.destroyOptions.resourceWarnAfterMs },
@@ -59887,7 +60075,7 @@ async function deployCommand(stacks, options) {
59887
60075
  let deployInterrupted = false;
59888
60076
  const topLevelSigintHandler = () => {
59889
60077
  if (deployInterrupted) {
59890
- process.stderr.write("\nForce-quit: stack locks may not be released. If the next run reports a lock, run: cdkd force-unlock <stackName>\n");
60078
+ process.stderr.write("\nForce-quit: stack locks may not be released. If the next run reports a lock, run this for EACH stack it names (the region-qualified form — see the message that run prints): cdkd force-unlock <stackName> --stack-region <region>\n");
59891
60079
  process.exit(130);
59892
60080
  }
59893
60081
  process.stderr.write("\nInterrupted — waiting for in-progress operations to complete...\n");
@@ -60154,6 +60342,10 @@ async function deployCommand(stacks, options) {
60154
60342
  dagBuilder,
60155
60343
  diffCalculator,
60156
60344
  options: deployEngineOptions,
60345
+ destroyOptions: {
60346
+ statePrefix: options.statePrefix,
60347
+ ...options.profile && { profile: options.profile }
60348
+ },
60157
60349
  ...assetRedirect && { assetRedirect }
60158
60350
  }, () => stackDeployEngine.deploy(stackInfo.stackName, stackInfo.template));
60159
60351
  logger.info(`\n${bold("Deployment Summary:")}`);
@@ -66124,6 +66316,11 @@ function setAtPath(target, path, value) {
66124
66316
  */
66125
66317
  async function runAccept(reports, stateBackend, stateConfig, awsClients, options) {
66126
66318
  const logger = getLogger();
66319
+ const lockRecovery = {
66320
+ profile: options.profile,
66321
+ stateBucket: stateConfig.bucket,
66322
+ statePrefix: stateConfig.prefix
66323
+ };
66127
66324
  printAcceptPlan(reports);
66128
66325
  if (options.dryRun) {
66129
66326
  logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
@@ -66146,7 +66343,12 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
66146
66343
  skipped: () => []
66147
66344
  }));
66148
66345
  if (driftedOutcomes.length === 0) continue;
66149
- await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept");
66346
+ if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-accept")) throw new Error(await buildLockContentionMessage({
66347
+ lockManager,
66348
+ stackName: report.stackName,
66349
+ region: report.region,
66350
+ recovery: lockRecovery
66351
+ }));
66150
66352
  try {
66151
66353
  const resources = { ...report.state.resources };
66152
66354
  for (const outcome of driftedOutcomes) {
@@ -66743,6 +66945,11 @@ function deepEqualUnordered(a, b) {
66743
66945
  */
66744
66946
  async function runRevert(reports, providerRegistry, stateBackend, stateConfig, awsClients, options) {
66745
66947
  const logger = getLogger();
66948
+ const lockRecovery = {
66949
+ profile: options.profile,
66950
+ stateBucket: stateConfig.bucket,
66951
+ statePrefix: stateConfig.prefix
66952
+ };
66746
66953
  printRevertPlan(reports);
66747
66954
  if (options.dryRun) {
66748
66955
  logger.info("--dry-run: AWS will NOT be modified. Re-run without --dry-run to apply.");
@@ -66770,11 +66977,16 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
66770
66977
  skipped: () => []
66771
66978
  }));
66772
66979
  if (driftedOutcomes.length === 0) continue;
66773
- await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
66980
+ if (!await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert")) throw new Error(await buildLockContentionMessage({
66981
+ lockManager,
66982
+ stackName: report.stackName,
66983
+ region: report.region,
66984
+ recovery: lockRecovery
66985
+ }));
66774
66986
  const narrowedByLogicalId = /* @__PURE__ */ new Map();
66775
- const revertSecretResolvers = new DriftSecretResolvers(report.region);
66776
- const revertProducerRegions = producerRegionsFromState(report.state);
66777
66987
  try {
66988
+ const revertSecretResolvers = new DriftSecretResolvers(report.region);
66989
+ const revertProducerRegions = producerRegionsFromState(report.state);
66778
66990
  await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
66779
66991
  const stateResource = report.state.resources[outcome.logicalId];
66780
66992
  if (!stateResource) {
@@ -67397,6 +67609,7 @@ async function destroyCommand(stackArgs, options) {
67397
67609
  exportIndexStore,
67398
67610
  destroyOptions: {
67399
67611
  ...options.profile && { profile: options.profile },
67612
+ statePrefix: options.statePrefix,
67400
67613
  ...options.removeProtection === true && { removeProtection: true },
67401
67614
  ...options.skipFinalSnapshot === true && { skipFinalSnapshot: true },
67402
67615
  ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
@@ -67412,6 +67625,7 @@ async function destroyCommand(stackArgs, options) {
67412
67625
  baseRegion: region,
67413
67626
  ...options.profile && { profile: options.profile },
67414
67627
  stateBucket,
67628
+ statePrefix: options.statePrefix,
67415
67629
  skipConfirmation: options.yes || options.force,
67416
67630
  removeProtection: options.removeProtection === true,
67417
67631
  skipFinalSnapshot: options.skipFinalSnapshot === true,
@@ -68985,6 +69199,11 @@ async function exportCommand(stackArg, options) {
68985
69199
  bucket: stateBucket,
68986
69200
  prefix: options.statePrefix
68987
69201
  };
69202
+ const lockRecovery = {
69203
+ profile: options.profile,
69204
+ stateBucket,
69205
+ statePrefix: options.statePrefix
69206
+ };
68988
69207
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
68989
69208
  ...options.region && { region: options.region },
68990
69209
  ...options.profile && { profile: options.profile }
@@ -69060,7 +69279,12 @@ async function exportCommand(stackArg, options) {
69060
69279
  }
69061
69280
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
69062
69281
  if (!options.dryRun) {
69063
- if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`);
69282
+ if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(await buildLockContentionMessage({
69283
+ lockManager,
69284
+ stackName: resolvedStackName,
69285
+ region: targetRegion,
69286
+ recovery: lockRecovery
69287
+ }));
69064
69288
  }
69065
69289
  try {
69066
69290
  if (nestedStackRows.length > 0) {
@@ -69080,6 +69304,7 @@ async function exportCommand(stackArg, options) {
69080
69304
  if (missingNested.length > 0) throw new Error(`Template requires parameter(s) without defaults: ${missingNested.join(", ")}. Pass each one as --parameter Key=Value (or set a Default in the CDK code).`);
69081
69305
  const childOverrides = parseCfnChildStackNameOverrides(options.cfnChildStackName);
69082
69306
  if ((await runPerStackImportLoop({
69307
+ lockRecovery,
69083
69308
  rootStackName: resolvedStackName,
69084
69309
  rootRegion: targetRegion,
69085
69310
  rootStackInfoNestedTemplates: rootNestedTemplatePaths,
@@ -70976,7 +71201,15 @@ async function runPerStackImportLoop(args) {
70976
71201
  try {
70977
71202
  await deps.lockManager.acquireLockWithRetry(node.stackName, node.region, deps.lockOwner, "export", CHILD_LOCK_RETRY_MAX_ATTEMPTS, CHILD_LOCK_RETRY_DELAY_MS);
70978
71203
  } catch (err) {
70979
- throw new Error(`Could not acquire lock for nested-stack child '${node.stackName}' (${node.region}) — another cdkd process held it through the retry window. Wait for it to finish, or run 'cdkd force-unlock ${node.stackName}' if you are certain no other process is active. No CloudFormation changeset has been submitted; cdkd state is unchanged.`, { cause: err instanceof Error ? err : void 0 });
71204
+ throw new Error(await buildLockContentionMessage({
71205
+ lockManager: deps.lockManager,
71206
+ stackName: node.stackName,
71207
+ region: node.region,
71208
+ subject: "nested-stack child",
71209
+ heldClause: "another cdkd process held it through the retry window",
71210
+ recovery: args.lockRecovery,
71211
+ suffix: "No CloudFormation changeset has been submitted; cdkd state is unchanged."
71212
+ }), { cause: err instanceof Error ? err : void 0 });
70980
71213
  }
70981
71214
  acquiredChildLocks.push({
70982
71215
  stackName: node.stackName,
@@ -71912,8 +72145,12 @@ async function stateOrphanCommand(stackArgs, options) {
71912
72145
  }
71913
72146
  if (!options.force) {
71914
72147
  for (const target of targets) if (await setup.lockManager.isLocked(stackName, target.region)) {
71915
- const where = target.region ?? "(legacy)";
71916
- throw new Error(`Stack '${stackName}' (${where}) is locked. Run 'cdkd force-unlock ${stackName}${target.region ? ` --stack-region ${target.region}` : ""}' first, or pass --force to remove anyway.`);
72148
+ const where = target.region ? displaySafe(target.region, { asciiOnly: true }) || "<unrenderable>" : "(legacy)";
72149
+ throw new Error(`Stack '${displaySafe(stackName, { asciiOnly: true })}' (${where}) is locked. Run: ${buildForceUnlockCommand(stackName, target.region, {
72150
+ profile: options.profile,
72151
+ stateBucket: setup.bucket,
72152
+ statePrefix: options.statePrefix
72153
+ }) || "a command cdkd cannot show safely: the recorded name or region would not survive a command line intact, so inspect the lock object directly"} first, or pass --force to remove anyway.`);
71917
72154
  }
71918
72155
  }
71919
72156
  if (!options.yes && !options.force) {
@@ -71933,9 +72170,13 @@ async function stateOrphanCommand(stackArgs, options) {
71933
72170
  }
71934
72171
  for (const target of targets) {
71935
72172
  if (target.region) {
72173
+ await warnOnLiveForeignLock(setup.lockManager, stackName, target.region, logger);
71936
72174
  await setup.stateBackend.deleteState(stackName, target.region);
71937
72175
  await setup.lockManager.forceReleaseLock(stackName, target.region);
71938
- } else await setup.lockManager.forceReleaseLock(stackName, void 0);
72176
+ } else {
72177
+ await warnOnLiveForeignLock(setup.lockManager, stackName, void 0, logger);
72178
+ await setup.lockManager.forceReleaseLock(stackName, void 0);
72179
+ }
71939
72180
  logger.info(`✓ Removed state for stack: ${formatStackRef(target)}`);
71940
72181
  }
71941
72182
  }
@@ -72067,6 +72308,7 @@ async function stateDestroyCommand(stackArgs, options) {
72067
72308
  exportIndexStore: setup.exportIndexStore,
72068
72309
  destroyOptions: {
72069
72310
  ...options.profile && { profile: options.profile },
72311
+ statePrefix: options.statePrefix,
72070
72312
  ...options.removeProtection === true && { removeProtection: true },
72071
72313
  ...options.skipFinalSnapshot === true && { skipFinalSnapshot: true },
72072
72314
  ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
@@ -72082,6 +72324,7 @@ async function stateDestroyCommand(stackArgs, options) {
72082
72324
  baseRegion: setup.region,
72083
72325
  ...options.profile && { profile: options.profile },
72084
72326
  stateBucket: setup.bucket,
72327
+ statePrefix: options.statePrefix,
72085
72328
  skipConfirmation: options.yes || options.all === true,
72086
72329
  removeProtection: options.removeProtection === true,
72087
72330
  skipFinalSnapshot: options.skipFinalSnapshot === true,
@@ -72443,7 +72686,12 @@ async function stateRefreshObservedCommand(stackArgs, options) {
72443
72686
  if (!target.region) throw new Error(`Stack '${target.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${target.stackName}' (or any cdkd write) first to migrate it to the region-scoped layout, then re-run refresh-observed.`);
72444
72687
  const counts = await refreshObservedForStack(target.stackName, target.region, setup.stateBackend, setup.lockManager, providerRegistry, {
72445
72688
  dryRun: options.dryRun ?? false,
72446
- logger
72689
+ logger,
72690
+ lockRecovery: {
72691
+ profile: options.profile,
72692
+ stateBucket: setup.bucket,
72693
+ statePrefix: options.statePrefix
72694
+ }
72447
72695
  });
72448
72696
  totalRefreshed += counts.refreshed;
72449
72697
  totalUnsupported += counts.unsupported;
@@ -72470,6 +72718,34 @@ async function stateRefreshObservedCommand(stackArgs, options) {
72470
72718
  */
72471
72719
  const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
72472
72720
  /**
72721
+ * Warn before `cdkd state orphan` force-releases a lock that is still LIVE.
72722
+ *
72723
+ * `forceReleaseLock` is unconditional by design — a stuck lock must never make
72724
+ * a state record unremovable — but it deletes an in-flight `cdkd deploy`'s lock
72725
+ * just as readily as a stale one, and it did so silently (issue #2171). The
72726
+ * write it enables has already happened by the time anyone notices, so the
72727
+ * only useful thing is to name the owner at the moment of the release.
72728
+ *
72729
+ * Best-effort in both directions: an expired lock is not worth a line, and a
72730
+ * failed read must not block the removal the user asked for.
72731
+ *
72732
+ * (Issue #2171's own text called this command `cdkd state rm`; no such command
72733
+ * exists -- this file registers it as `orphan`.)
72734
+ */
72735
+ async function warnOnLiveForeignLock(lockManager, stackName, region, logger) {
72736
+ try {
72737
+ const info = await lockManager.getLockInfo(stackName, region);
72738
+ if (!info) return;
72739
+ const expiryKnown = Number.isFinite(info.expiresAt);
72740
+ if (expiryKnown && info.expiresAt <= Date.now()) return;
72741
+ const safeStack = displaySafe(stackName, { asciiOnly: true }) || "<unrenderable>";
72742
+ const where = region ? `${safeStack} (${displaySafe(region, { asciiOnly: true }) || "<unrenderable>"})` : `${safeStack} (legacy lock key)`;
72743
+ const owner = info.owner;
72744
+ const operation = info.operation ? `, operation: ${info.operation}` : "";
72745
+ logger.warn(`Force-releasing a LIVE lock on ${where} ${owner ? `held by ${owner}${operation}` : "held by an unnamed holder"}. ` + (owner && expiryKnown ? `That process is still running and will keep writing; its next state write ` : expiryKnown ? `The lock has not expired, so a process may still be writing; its next state write ` : `The lock records no readable expiry, so a process may still be writing; its next state write `) + `may recreate the record being removed here.`);
72746
+ } catch {}
72747
+ }
72748
+ /**
72473
72749
  * Refresh the `observedProperties` of every resource in one stack
72474
72750
  * record. Returns counts so the caller can aggregate across `--all`.
72475
72751
  *
@@ -72479,7 +72755,7 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
72479
72755
  * for the underlying SDK reads.
72480
72756
  */
72481
72757
  async function refreshObservedForStack(stackName, region, stateBackend, lockManager, providerRegistry, opts) {
72482
- const { logger } = opts;
72758
+ const { logger, lockRecovery } = opts;
72483
72759
  const result = await stateBackend.getState(stackName, region);
72484
72760
  if (!result) throw new Error(`No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`);
72485
72761
  const { state, etag, migrationPending } = result;
@@ -72517,7 +72793,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
72517
72793
  };
72518
72794
  }
72519
72795
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
72520
- await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed");
72796
+ if (!await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed")) throw new Error(await buildLockContentionMessage({
72797
+ lockManager,
72798
+ stackName,
72799
+ region,
72800
+ recovery: lockRecovery
72801
+ }));
72521
72802
  try {
72522
72803
  let refreshed = 0;
72523
72804
  let unsupported = 0;
@@ -72860,7 +73141,10 @@ async function rollbackCommand(stackArg, options) {
72860
73141
  awsClients: setup.awsClients,
72861
73142
  stateBucket: setup.bucket,
72862
73143
  exportIndexStore: setup.exportIndexStore,
72863
- destroyOptions: { ...options.profile && { profile: options.profile } }
73144
+ destroyOptions: {
73145
+ ...options.profile && { profile: options.profile },
73146
+ statePrefix: options.statePrefix
73147
+ }
72864
73148
  }, () => withStackName(stackName, async () => {
72865
73149
  let failedOpFailures = 0;
72866
73150
  let failedOpWarnings = 0;
@@ -74896,7 +75180,18 @@ async function orphanCommand(pathArgs, options) {
74896
75180
  const targetRegion = await pickStackRegion(stateBackend, stackInfo.stackName, stackInfo.region, options.stackRegion);
74897
75181
  logger.info(`Target: ${stackInfo.stackName} (${targetRegion}); orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`);
74898
75182
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
74899
- if (!options.dryRun) await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan");
75183
+ if (!options.dryRun) {
75184
+ if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan")) throw new Error(await buildLockContentionMessage({
75185
+ lockManager,
75186
+ stackName: stackInfo.stackName,
75187
+ region: targetRegion,
75188
+ recovery: {
75189
+ profile: options.profile,
75190
+ stateBucket,
75191
+ statePrefix: options.statePrefix
75192
+ }
75193
+ }));
75194
+ }
74900
75195
  try {
74901
75196
  const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
74902
75197
  if (!stateData) throw new Error(`No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`);
@@ -75381,6 +75676,11 @@ async function importCommand(stackArg, options) {
75381
75676
  bucket: stateBucket,
75382
75677
  prefix: options.statePrefix
75383
75678
  };
75679
+ const lockRecovery = {
75680
+ profile: options.profile,
75681
+ stateBucket,
75682
+ statePrefix: options.statePrefix
75683
+ };
75384
75684
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
75385
75685
  ...options.region && { region: options.region },
75386
75686
  ...options.profile && { profile: options.profile }
@@ -75481,7 +75781,16 @@ async function importCommand(stackArg, options) {
75481
75781
  }
75482
75782
  for (const overrideId of overrides.keys()) if (!templateLogicalIds.has(overrideId)) throw new Error(`--resource / --resource-mapping references logical ID '${overrideId}' which is not in the synthesized template for stack '${stackInfo.stackName}'. Available IDs: ${[...templateLogicalIds].join(", ")}`);
75483
75783
  const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
75484
- await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "import");
75784
+ if (!await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "import")) throw new Error(await buildLockContentionMessage({
75785
+ lockManager,
75786
+ stackName: stackInfo.stackName,
75787
+ region: targetRegion,
75788
+ recovery: {
75789
+ profile: options.profile,
75790
+ stateBucket,
75791
+ statePrefix: options.statePrefix
75792
+ }
75793
+ }));
75485
75794
  try {
75486
75795
  const rows = [];
75487
75796
  for (const { logicalId, resource } of resources) {
@@ -75545,6 +75854,7 @@ async function importCommand(stackArg, options) {
75545
75854
  logger.info(`✓ State written: ${stackInfo.stackName} (${targetRegion})`);
75546
75855
  logger.info(` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`);
75547
75856
  if (migrationCfnStackName && migrationTree && accountIdForNestedSynth) await importNestedStackChildrenRecursive({
75857
+ lockRecovery,
75548
75858
  parentStackName: stackInfo.stackName,
75549
75859
  parentRegion: targetRegion,
75550
75860
  parentNestedTemplates: stackInfo.nestedTemplates ?? {},
@@ -76210,7 +76520,13 @@ async function importNestedStackChildrenRecursive(args) {
76210
76520
  const childRewritten = rewriteTemplateAssetReferences(childTemplate, assetRedirect);
76211
76521
  if (childRewritten > 0) logger.info(`Note: ${childRewritten} asset reference(s) in nested stack ${childStackName} are recorded in state at their pre-rewrite (CDK bootstrap) values, so the next 'cdkd deploy' repoints any resource that still holds them to cdkd asset storage.`);
76212
76522
  }
76213
- await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import");
76523
+ if (!await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import")) throw new Error(await buildLockContentionMessage({
76524
+ lockManager,
76525
+ stackName: childStackName,
76526
+ region: childRegion,
76527
+ subject: "nested stack",
76528
+ recovery: args.lockRecovery
76529
+ }));
76214
76530
  try {
76215
76531
  const childResources = collectImportableResources(childTemplate);
76216
76532
  const childTemplateLogicalIds = new Set(childResources.map((r) => r.logicalId));
@@ -76252,6 +76568,7 @@ async function importNestedStackChildrenRecursive(args) {
76252
76568
  await stateBackend.saveState(childStackName, childRegion, childStackState);
76253
76569
  logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
76254
76570
  if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
76571
+ lockRecovery: args.lockRecovery,
76255
76572
  parentStackName: childStackName,
76256
76573
  parentRegion: childRegion,
76257
76574
  parentNestedTemplates: indexGrandchildTemplatePaths(childTemplate, childTemplatePath),
@@ -86125,7 +86442,7 @@ function createMigrateCommand() {
86125
86442
  */
86126
86443
  function buildProgram() {
86127
86444
  const program = new Command();
86128
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.37");
86445
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.39");
86129
86446
  program.hook("preAction", (_thisCommand, actionCommand) => {
86130
86447
  const { profile } = actionCommand.optsWithGlobals();
86131
86448
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;