@go-to-k/cdkd 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -528,29 +528,82 @@ function parseDuration(value) {
528
528
  const multiplier = unit === "s" ? 1e3 : unit === "m" ? 6e4 : 36e5;
529
529
  return Math.round(num * multiplier);
530
530
  }
531
+ var RESOURCE_TYPE_REGEX = /^[A-Z][A-Za-z0-9]+::[A-Z][A-Za-z0-9]+::[A-Z][A-Za-z0-9]+$/;
532
+ function parseResourceTimeoutToken(flagName) {
533
+ return (raw, previous) => {
534
+ const acc = previous ?? { perTypeMs: {} };
535
+ if (!acc.perTypeMs)
536
+ acc.perTypeMs = {};
537
+ const eqIndex = raw.indexOf("=");
538
+ if (eqIndex === -1) {
539
+ acc.globalMs = parseDuration(raw);
540
+ return acc;
541
+ }
542
+ const typePart = raw.substring(0, eqIndex).trim();
543
+ const durationPart = raw.substring(eqIndex + 1).trim();
544
+ if (!RESOURCE_TYPE_REGEX.test(typePart)) {
545
+ throw new Error(
546
+ `Invalid ${flagName} value "${raw}": left-hand side must be a CloudFormation resource type like AWS::Service::Resource (got "${typePart}")`
547
+ );
548
+ }
549
+ if (durationPart.length === 0) {
550
+ throw new Error(
551
+ `Invalid ${flagName} value "${raw}": missing duration after '=' (e.g. ${typePart}=1h)`
552
+ );
553
+ }
554
+ let ms;
555
+ try {
556
+ ms = parseDuration(durationPart);
557
+ } catch (err) {
558
+ const inner = err instanceof Error ? err.message : String(err);
559
+ throw new Error(`Invalid ${flagName} value "${raw}": ${inner}`);
560
+ }
561
+ acc.perTypeMs[typePart] = ms;
562
+ return acc;
563
+ };
564
+ }
531
565
  var resourceTimeoutOptions = [
532
- // Default values are stored as parsed milliseconds (NOT the source
533
- // string) because commander's `argParser` only runs on user-supplied
534
- // values, never on defaults without this every command handler
535
- // would see `'5m'` (string) when the user did not pass the flag and
536
- // `300_000` (number) when they did. The second `defaultValueDescription`
537
- // argument keeps `--help` showing the human-readable form.
566
+ // Default is `undefined` (NOT a pre-seeded ResourceTimeoutOption) the
567
+ // command handler resolves missing globalMs to DEFAULT_RESOURCE_*_MS
568
+ // at the call site. Pre-seeding here would force every accumulator
569
+ // call to carry a snapshot, and would also surprise unit tests that
570
+ // expect `opts.resourceTimeout` to be `undefined` when the flag is not
571
+ // passed.
538
572
  new Option(
539
- "--resource-warn-after <duration>",
540
- "Warn when a single resource operation exceeds this wall-clock duration (e.g. 5m, 90s, 1h)"
541
- ).default(parseDuration("5m"), "5m").argParser(parseDuration),
573
+ "--resource-warn-after <duration_or_type=duration>",
574
+ "Warn when a single resource operation exceeds this wall-clock duration. Repeatable: pass a bare duration (e.g. 5m) to set the global default, or TYPE=DURATION (e.g. AWS::CloudFront::Distribution=10m) for a per-type override."
575
+ ).default(void 0, "5m").argParser(parseResourceTimeoutToken("--resource-warn-after")),
542
576
  new Option(
543
- "--resource-timeout <duration>",
544
- "Abort a single resource operation that exceeds this wall-clock duration. Custom-Resource-heavy stacks may need to raise this above the default 30m (the Custom Resource provider's polling cap is 1h)."
545
- ).default(parseDuration("30m"), "30m").argParser(parseDuration)
577
+ "--resource-timeout <duration_or_type=duration>",
578
+ "Abort a single resource operation that exceeds this wall-clock duration. Repeatable: pass a bare duration (e.g. 30m) to set the global default, or TYPE=DURATION (e.g. AWS::CloudFront::Distribution=1h) for a per-type override. Custom-Resource-heavy stacks may need to raise this above the default 30m (the Custom Resource provider's polling cap is 1h)."
579
+ ).default(void 0, "30m").argParser(parseResourceTimeoutToken("--resource-timeout"))
546
580
  ];
547
581
  function validateResourceTimeouts(opts) {
548
582
  const warn = opts.resourceWarnAfter;
549
583
  const timeout = opts.resourceTimeout;
550
- if (typeof warn === "number" && typeof timeout === "number" && warn >= timeout) {
551
- throw new Error(
552
- `--resource-warn-after (${warn}ms) must be less than --resource-timeout (${timeout}ms)`
553
- );
584
+ const globalWarn = warn?.globalMs;
585
+ const globalTimeout = timeout?.globalMs;
586
+ if (typeof globalWarn === "number" && typeof globalTimeout === "number") {
587
+ if (globalWarn >= globalTimeout) {
588
+ throw new Error(
589
+ `--resource-warn-after (${globalWarn}ms) must be less than --resource-timeout (${globalTimeout}ms)`
590
+ );
591
+ }
592
+ }
593
+ const warnPerType = warn?.perTypeMs ?? {};
594
+ const timeoutPerType = timeout?.perTypeMs ?? {};
595
+ const types = /* @__PURE__ */ new Set([...Object.keys(warnPerType), ...Object.keys(timeoutPerType)]);
596
+ for (const t of types) {
597
+ const effectiveWarn = warnPerType[t] ?? globalWarn;
598
+ const effectiveTimeout = timeoutPerType[t] ?? globalTimeout;
599
+ if (typeof effectiveWarn !== "number" || typeof effectiveTimeout !== "number") {
600
+ continue;
601
+ }
602
+ if (effectiveWarn >= effectiveTimeout) {
603
+ throw new Error(
604
+ `--resource-warn-after for ${t} (${effectiveWarn}ms) must be less than --resource-timeout for ${t} (${effectiveTimeout}ms)`
605
+ );
606
+ }
554
607
  }
555
608
  }
556
609
  var deployOptions = [
@@ -31580,8 +31633,8 @@ var DeployEngine = class {
31580
31633
  const baseLabel = `${verb} ${logicalId} (${resourceType})`;
31581
31634
  renderer.addTask(logicalId, baseLabel);
31582
31635
  const operationKind = change.changeType === "CREATE" ? "CREATE" : change.changeType === "DELETE" ? "DELETE" : "UPDATE";
31583
- const warnAfterMs = this.options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
31584
- const timeoutMs = this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
31636
+ const warnAfterMs = this.options.resourceWarnAfterByType?.[resourceType] ?? this.options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
31637
+ const timeoutMs = this.options.resourceTimeoutByType?.[resourceType] ?? this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
31585
31638
  try {
31586
31639
  await withResourceDeadline(
31587
31640
  async () => {
@@ -32085,8 +32138,8 @@ async function deployCommand(stacks, options) {
32085
32138
  }
32086
32139
  warnIfDeprecatedRegion(options);
32087
32140
  validateResourceTimeouts({
32088
- resourceWarnAfter: options.resourceWarnAfter,
32089
- resourceTimeout: options.resourceTimeout
32141
+ ...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
32142
+ ...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
32090
32143
  });
32091
32144
  if (!options.wait) {
32092
32145
  process.env["CDKD_NO_WAIT"] = "true";
@@ -32281,8 +32334,18 @@ Deploying stack: ${stackInfo.stackName}${stackRegion !== baseRegion ? ` (region:
32281
32334
  concurrency: options.concurrency,
32282
32335
  dryRun: options.dryRun,
32283
32336
  noRollback: !options.rollback,
32284
- resourceWarnAfterMs: options.resourceWarnAfter,
32285
- resourceTimeoutMs: options.resourceTimeout
32337
+ ...options.resourceWarnAfter?.globalMs !== void 0 && {
32338
+ resourceWarnAfterMs: options.resourceWarnAfter.globalMs
32339
+ },
32340
+ ...options.resourceTimeout?.globalMs !== void 0 && {
32341
+ resourceTimeoutMs: options.resourceTimeout.globalMs
32342
+ },
32343
+ ...options.resourceWarnAfter?.perTypeMs && {
32344
+ resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs
32345
+ },
32346
+ ...options.resourceTimeout?.perTypeMs && {
32347
+ resourceTimeoutByType: options.resourceTimeout.perTypeMs
32348
+ }
32286
32349
  },
32287
32350
  stackRegion
32288
32351
  );
@@ -32692,8 +32755,6 @@ Acquiring lock for stack ${stackName}...`);
32692
32755
  logger.debug(
32693
32756
  `Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`
32694
32757
  );
32695
- const warnAfterMs = ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
32696
- const timeoutMs = ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
32697
32758
  const stackRegion2 = state.region ?? ctx.baseRegion;
32698
32759
  const deletePromises = level.map(async (logicalId) => {
32699
32760
  const resource = state.resources[logicalId];
@@ -32701,6 +32762,8 @@ Acquiring lock for stack ${stackName}...`);
32701
32762
  logger.warn(`Resource ${logicalId} not found in state, skipping`);
32702
32763
  return;
32703
32764
  }
32765
+ const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
32766
+ const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
32704
32767
  const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
32705
32768
  renderer.addTask(logicalId, baseLabel);
32706
32769
  try {
@@ -32821,8 +32884,8 @@ async function destroyCommand(stackArgs, options) {
32821
32884
  }
32822
32885
  warnIfDeprecatedRegion(options);
32823
32886
  validateResourceTimeouts({
32824
- resourceWarnAfter: options.resourceWarnAfter,
32825
- resourceTimeout: options.resourceTimeout
32887
+ ...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
32888
+ ...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
32826
32889
  });
32827
32890
  const region = options.region || process.env["AWS_REGION"] || "us-east-1";
32828
32891
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
@@ -32956,8 +33019,18 @@ Preparing to destroy stack: ${stackName}`);
32956
33019
  ...options.profile && { profile: options.profile },
32957
33020
  stateBucket,
32958
33021
  skipConfirmation: options.yes || options.force,
32959
- resourceWarnAfterMs: options.resourceWarnAfter,
32960
- resourceTimeoutMs: options.resourceTimeout
33022
+ ...options.resourceWarnAfter?.globalMs !== void 0 && {
33023
+ resourceWarnAfterMs: options.resourceWarnAfter.globalMs
33024
+ },
33025
+ ...options.resourceTimeout?.globalMs !== void 0 && {
33026
+ resourceTimeoutMs: options.resourceTimeout.globalMs
33027
+ },
33028
+ ...options.resourceWarnAfter?.perTypeMs && {
33029
+ resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs
33030
+ },
33031
+ ...options.resourceTimeout?.perTypeMs && {
33032
+ resourceTimeoutByType: options.resourceTimeout.perTypeMs
33033
+ }
32961
33034
  });
32962
33035
  }
32963
33036
  } finally {
@@ -32986,15 +33059,432 @@ function createDestroyCommand() {
32986
33059
  import * as readline2 from "node:readline/promises";
32987
33060
  import { Command as Command7 } from "commander";
32988
33061
  init_aws_clients();
32989
- async function orphanCommand(stackArgs, options) {
33062
+
33063
+ // src/cli/cdk-path.ts
33064
+ function readCdkPath(resource) {
33065
+ const meta = resource.Metadata;
33066
+ if (!meta)
33067
+ return "";
33068
+ const v = meta["aws:cdk:path"];
33069
+ return typeof v === "string" ? v : "";
33070
+ }
33071
+ function buildCdkPathIndex(template) {
33072
+ const index = /* @__PURE__ */ new Map();
33073
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
33074
+ const path = readCdkPath(resource);
33075
+ if (path)
33076
+ index.set(path, logicalId);
33077
+ }
33078
+ return index;
33079
+ }
33080
+
33081
+ // src/analyzer/orphan-rewriter.ts
33082
+ var AttributeFetcher = class {
33083
+ constructor(orphans, providerRegistry, options) {
33084
+ this.orphans = orphans;
33085
+ this.providerRegistry = providerRegistry;
33086
+ this.options = options;
33087
+ }
33088
+ cache = /* @__PURE__ */ new Map();
33089
+ logger = getLogger().child("OrphanRewriter");
33090
+ /**
33091
+ * Return the orphan's resolved value for `Ref` (its physicalId) — never
33092
+ * needs an AWS call.
33093
+ */
33094
+ ref(orphanLogicalId) {
33095
+ const o = this.orphans[orphanLogicalId];
33096
+ if (!o) {
33097
+ throw new Error(
33098
+ `Internal: Ref to '${orphanLogicalId}' has no orphan entry \u2014 should have been filtered out`
33099
+ );
33100
+ }
33101
+ return o.physicalId;
33102
+ }
33103
+ /**
33104
+ * Return the orphan's resolved value for `Fn::GetAtt`. Hits the live
33105
+ * provider on first call; subsequent calls reuse the cached result.
33106
+ *
33107
+ * Returns `{ ok: true, value }` on success; `{ ok: false, reason }`
33108
+ * when the live fetch failed AND the `--force` cache fallback either
33109
+ * was disabled or also lacked the attribute. In the cache-fallback
33110
+ * success path returns `{ ok: true, value, fromCache: true }`.
33111
+ */
33112
+ async getAtt(orphanLogicalId, attribute) {
33113
+ const cacheKey = `${orphanLogicalId}\0${attribute}`;
33114
+ if (this.cache.has(cacheKey)) {
33115
+ return { ok: true, value: this.cache.get(cacheKey) };
33116
+ }
33117
+ const orphan = this.orphans[orphanLogicalId];
33118
+ if (!orphan) {
33119
+ return {
33120
+ ok: false,
33121
+ reason: `Internal: GetAtt to '${orphanLogicalId}' has no orphan entry`
33122
+ };
33123
+ }
33124
+ let provider;
33125
+ try {
33126
+ provider = this.providerRegistry.getProvider(orphan.resourceType);
33127
+ } catch (err) {
33128
+ return {
33129
+ ok: false,
33130
+ reason: `no provider available for ${orphan.resourceType}: ${err instanceof Error ? err.message : String(err)}`
33131
+ };
33132
+ }
33133
+ if (!provider.getAttribute) {
33134
+ return this.cacheFallback(
33135
+ orphanLogicalId,
33136
+ attribute,
33137
+ `provider for ${orphan.resourceType} does not implement getAttribute`
33138
+ );
33139
+ }
33140
+ try {
33141
+ const value = await provider.getAttribute(orphan.physicalId, orphan.resourceType, attribute);
33142
+ if (value === void 0) {
33143
+ return this.cacheFallback(
33144
+ orphanLogicalId,
33145
+ attribute,
33146
+ `provider returned undefined for ${orphan.resourceType}.${attribute}`
33147
+ );
33148
+ }
33149
+ this.cache.set(cacheKey, value);
33150
+ return { ok: true, value };
33151
+ } catch (err) {
33152
+ return this.cacheFallback(
33153
+ orphanLogicalId,
33154
+ attribute,
33155
+ err instanceof Error ? err.message : String(err)
33156
+ );
33157
+ }
33158
+ }
33159
+ /**
33160
+ * Try the orphan's `state.attributes[attribute]` as a last-resort value
33161
+ * source under `--force`. Without `--force`, returns the original
33162
+ * failure reason unchanged (caller pushes to `unresolvable`).
33163
+ */
33164
+ cacheFallback(orphanLogicalId, attribute, reason) {
33165
+ if (!this.options.force) {
33166
+ return { ok: false, reason };
33167
+ }
33168
+ const orphan = this.orphans[orphanLogicalId];
33169
+ const cached = orphan.attributes?.[attribute];
33170
+ if (cached === void 0) {
33171
+ this.logger.warn(
33172
+ `--force: state.attributes also lacks '${orphanLogicalId}.${attribute}'; leaving the original intrinsic in place.`
33173
+ );
33174
+ return {
33175
+ ok: false,
33176
+ reason: `${reason}; state.attributes cache also has no value for '${attribute}'`
33177
+ };
33178
+ }
33179
+ this.logger.warn(
33180
+ `--force: live fetch failed for '${orphanLogicalId}.${attribute}' (${reason}); falling back to cached value from state.attributes.`
33181
+ );
33182
+ const cacheKey = `${orphanLogicalId}\0${attribute}`;
33183
+ this.cache.set(cacheKey, cached);
33184
+ return { ok: true, value: cached, fromCache: true };
33185
+ }
33186
+ };
33187
+ async function rewriteResourceReferences(state, orphanLogicalIds, providerRegistry, options = {}) {
33188
+ const orphanSet = new Set(orphanLogicalIds);
33189
+ const orphans = {};
33190
+ for (const id of orphanLogicalIds) {
33191
+ const r = state.resources[id];
33192
+ if (!r) {
33193
+ throw new Error(`rewriteResourceReferences: orphan '${id}' not found in state.resources`);
33194
+ }
33195
+ orphans[id] = r;
33196
+ }
33197
+ const fetcher = new AttributeFetcher(orphans, providerRegistry, options);
33198
+ const rewrites = [];
33199
+ const unresolvable = [];
33200
+ const newResources = {};
33201
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
33202
+ if (orphanSet.has(logicalId))
33203
+ continue;
33204
+ const rewrittenProperties = await rewriteValue(
33205
+ resource.properties,
33206
+ `properties`,
33207
+ logicalId,
33208
+ orphanSet,
33209
+ fetcher,
33210
+ rewrites,
33211
+ unresolvable
33212
+ );
33213
+ const rewrittenAttributes = resource.attributes ? await rewriteValue(
33214
+ resource.attributes,
33215
+ `attributes`,
33216
+ logicalId,
33217
+ orphanSet,
33218
+ fetcher,
33219
+ rewrites,
33220
+ unresolvable
33221
+ ) : void 0;
33222
+ const newDeps = (resource.dependencies ?? []).filter((dep) => {
33223
+ if (orphanSet.has(dep)) {
33224
+ rewrites.push({
33225
+ logicalId,
33226
+ path: "dependencies",
33227
+ kind: "dependency",
33228
+ before: dep,
33229
+ after: null,
33230
+ orphanLogicalId: dep
33231
+ });
33232
+ return false;
33233
+ }
33234
+ return true;
33235
+ });
33236
+ newResources[logicalId] = {
33237
+ ...resource,
33238
+ properties: rewrittenProperties,
33239
+ ...rewrittenAttributes !== void 0 && {
33240
+ attributes: rewrittenAttributes
33241
+ },
33242
+ dependencies: newDeps
33243
+ };
33244
+ }
33245
+ const newOutputs = {};
33246
+ for (const [name, value] of Object.entries(state.outputs ?? {})) {
33247
+ newOutputs[name] = await rewriteValue(
33248
+ value,
33249
+ `outputs.${name}`,
33250
+ `<output:${name}>`,
33251
+ orphanSet,
33252
+ fetcher,
33253
+ rewrites,
33254
+ unresolvable
33255
+ );
33256
+ }
33257
+ return {
33258
+ state: {
33259
+ ...state,
33260
+ resources: newResources,
33261
+ outputs: newOutputs,
33262
+ lastModified: Date.now()
33263
+ },
33264
+ rewrites,
33265
+ unresolvable
33266
+ };
33267
+ }
33268
+ async function rewriteValue(value, pathPrefix, ownerLogicalId, orphanSet, fetcher, rewrites, unresolvable) {
33269
+ if (typeof value !== "object" || value === null)
33270
+ return value;
33271
+ if (Array.isArray(value)) {
33272
+ const out2 = [];
33273
+ for (let i = 0; i < value.length; i++) {
33274
+ out2.push(
33275
+ await rewriteValue(
33276
+ value[i],
33277
+ `${pathPrefix}[${i}]`,
33278
+ ownerLogicalId,
33279
+ orphanSet,
33280
+ fetcher,
33281
+ rewrites,
33282
+ unresolvable
33283
+ )
33284
+ );
33285
+ }
33286
+ return out2;
33287
+ }
33288
+ const obj = value;
33289
+ if ("Ref" in obj && Object.keys(obj).length === 1 && typeof obj["Ref"] === "string") {
33290
+ const target = obj["Ref"];
33291
+ if (orphanSet.has(target)) {
33292
+ const replaced = fetcher.ref(target);
33293
+ rewrites.push({
33294
+ logicalId: ownerLogicalId,
33295
+ path: pathPrefix,
33296
+ kind: "ref",
33297
+ before: { Ref: target },
33298
+ after: replaced,
33299
+ orphanLogicalId: target
33300
+ });
33301
+ return replaced;
33302
+ }
33303
+ return value;
33304
+ }
33305
+ if ("Fn::GetAtt" in obj && Object.keys(obj).length === 1) {
33306
+ const arg = obj["Fn::GetAtt"];
33307
+ let target;
33308
+ let attribute;
33309
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
33310
+ target = arg[0];
33311
+ attribute = arg[1];
33312
+ } else if (typeof arg === "string") {
33313
+ const dot = arg.indexOf(".");
33314
+ if (dot > 0) {
33315
+ target = arg.slice(0, dot);
33316
+ attribute = arg.slice(dot + 1);
33317
+ }
33318
+ }
33319
+ if (target && attribute && orphanSet.has(target)) {
33320
+ const result = await fetcher.getAtt(target, attribute);
33321
+ if (result.ok) {
33322
+ rewrites.push({
33323
+ logicalId: ownerLogicalId,
33324
+ path: pathPrefix,
33325
+ kind: "getAtt",
33326
+ before: { "Fn::GetAtt": [target, attribute] },
33327
+ after: result.value,
33328
+ orphanLogicalId: target
33329
+ });
33330
+ return result.value;
33331
+ }
33332
+ unresolvable.push({
33333
+ logicalId: ownerLogicalId,
33334
+ path: pathPrefix,
33335
+ orphanLogicalId: target,
33336
+ attribute,
33337
+ reason: result.reason
33338
+ });
33339
+ return value;
33340
+ }
33341
+ return value;
33342
+ }
33343
+ if ("Fn::Sub" in obj && Object.keys(obj).length === 1) {
33344
+ const arg = obj["Fn::Sub"];
33345
+ let template;
33346
+ let varMap;
33347
+ if (typeof arg === "string") {
33348
+ template = arg;
33349
+ } else if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "object" && arg[1] !== null) {
33350
+ template = arg[0];
33351
+ varMap = arg[1];
33352
+ }
33353
+ if (template !== void 0) {
33354
+ const { rewritten, didChange, hasUnresolvable } = await rewriteSubTemplate(
33355
+ template,
33356
+ ownerLogicalId,
33357
+ pathPrefix,
33358
+ orphanSet,
33359
+ fetcher,
33360
+ rewrites,
33361
+ unresolvable,
33362
+ varMap
33363
+ );
33364
+ if (didChange) {
33365
+ const stillHasIntrinsics = /\$\{[^}]+\}/.test(rewritten);
33366
+ if (varMap && stillHasIntrinsics) {
33367
+ return { "Fn::Sub": [rewritten, varMap] };
33368
+ }
33369
+ if (stillHasIntrinsics) {
33370
+ return { "Fn::Sub": rewritten };
33371
+ }
33372
+ return rewritten;
33373
+ }
33374
+ return value;
33375
+ }
33376
+ return value;
33377
+ }
33378
+ const out = {};
33379
+ for (const [k, v] of Object.entries(obj)) {
33380
+ out[k] = await rewriteValue(
33381
+ v,
33382
+ pathPrefix === "" ? k : `${pathPrefix}.${k}`,
33383
+ ownerLogicalId,
33384
+ orphanSet,
33385
+ fetcher,
33386
+ rewrites,
33387
+ unresolvable
33388
+ );
33389
+ }
33390
+ return out;
33391
+ }
33392
+ async function rewriteSubTemplate(template, ownerLogicalId, pathPrefix, orphanSet, fetcher, rewrites, unresolvable, varMap) {
33393
+ const placeholderRe = /\$\{([^}]+)\}/g;
33394
+ const matches = [...template.matchAll(placeholderRe)];
33395
+ if (matches.length === 0) {
33396
+ return { rewritten: template, didChange: false, hasUnresolvable: false };
33397
+ }
33398
+ let didChange = false;
33399
+ let hasUnresolvable = false;
33400
+ let cursor = 0;
33401
+ let out = "";
33402
+ for (const m of matches) {
33403
+ const inner = m[1] ?? "";
33404
+ const start = m.index ?? 0;
33405
+ out += template.slice(cursor, start);
33406
+ cursor = start + m[0].length;
33407
+ if (varMap && inner in varMap) {
33408
+ out += m[0];
33409
+ continue;
33410
+ }
33411
+ const dot = inner.indexOf(".");
33412
+ if (dot < 0) {
33413
+ if (orphanSet.has(inner)) {
33414
+ const replaced = fetcher.ref(inner);
33415
+ rewrites.push({
33416
+ logicalId: ownerLogicalId,
33417
+ path: pathPrefix,
33418
+ kind: "sub",
33419
+ before: m[0],
33420
+ after: replaced,
33421
+ orphanLogicalId: inner
33422
+ });
33423
+ out += replaced;
33424
+ didChange = true;
33425
+ } else {
33426
+ out += m[0];
33427
+ }
33428
+ } else {
33429
+ const target = inner.slice(0, dot);
33430
+ const attribute = inner.slice(dot + 1);
33431
+ if (orphanSet.has(target)) {
33432
+ const result = await fetcher.getAtt(target, attribute);
33433
+ if (result.ok) {
33434
+ const stringified = String(result.value);
33435
+ rewrites.push({
33436
+ logicalId: ownerLogicalId,
33437
+ path: pathPrefix,
33438
+ kind: "sub",
33439
+ before: m[0],
33440
+ after: stringified,
33441
+ orphanLogicalId: target
33442
+ });
33443
+ out += stringified;
33444
+ didChange = true;
33445
+ } else {
33446
+ unresolvable.push({
33447
+ logicalId: ownerLogicalId,
33448
+ path: pathPrefix,
33449
+ orphanLogicalId: target,
33450
+ attribute,
33451
+ reason: result.reason
33452
+ });
33453
+ out += m[0];
33454
+ hasUnresolvable = true;
33455
+ }
33456
+ } else {
33457
+ out += m[0];
33458
+ }
33459
+ }
33460
+ }
33461
+ out += template.slice(cursor);
33462
+ return { rewritten: out, didChange, hasUnresolvable };
33463
+ }
33464
+
33465
+ // src/cli/commands/orphan.ts
33466
+ async function orphanCommand(pathArgs, options) {
32990
33467
  const logger = getLogger();
32991
33468
  if (options.verbose)
32992
33469
  logger.setLevel("debug");
32993
33470
  warnIfDeprecatedRegion(options);
33471
+ if (pathArgs.length === 0) {
33472
+ throw new Error(
33473
+ "'cdkd orphan' requires at least one construct path, e.g. 'cdkd orphan MyStack/MyTable'.\n To remove a stack's state record (the previous behavior), use:\n cdkd state orphan MyStack"
33474
+ );
33475
+ }
33476
+ for (const p of pathArgs) {
33477
+ if (!p.includes("/")) {
33478
+ throw new Error(
33479
+ `'cdkd orphan' now expects a construct path like 'MyStack/MyTable'.
33480
+ Got: '${p}'
33481
+ To remove a stack's state record (the previous behavior), use:
33482
+ cdkd state orphan ${p}`
33483
+ );
33484
+ }
33485
+ }
32994
33486
  const region = options.region || process.env["AWS_REGION"] || "us-east-1";
32995
33487
  const stateBucket = await resolveStateBucketWithDefault(options.stateBucket, region);
32996
- logger.info("Starting stack orphan...");
32997
- logger.debug("Options:", options);
32998
33488
  if (options.region) {
32999
33489
  process.env["AWS_REGION"] = options.region;
33000
33490
  process.env["AWS_DEFAULT_REGION"] = options.region;
@@ -33005,10 +33495,7 @@ async function orphanCommand(stackArgs, options) {
33005
33495
  });
33006
33496
  setAwsClients(awsClients);
33007
33497
  try {
33008
- const stateConfig = {
33009
- bucket: stateBucket,
33010
- prefix: options.statePrefix
33011
- };
33498
+ const stateConfig = { bucket: stateBucket, prefix: options.statePrefix };
33012
33499
  const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
33013
33500
  ...options.region && { region: options.region },
33014
33501
  ...options.profile && { profile: options.profile }
@@ -33016,150 +33503,245 @@ async function orphanCommand(stackArgs, options) {
33016
33503
  await stateBackend.verifyBucketExists();
33017
33504
  const lockManager = new LockManager(awsClients.s3, stateConfig);
33018
33505
  const appCmd = options.app || resolveApp();
33019
- let appStacks = [];
33020
- if (appCmd) {
33021
- try {
33022
- const synthesizer = new Synthesizer();
33023
- const context = parseContextOptions(options.context);
33024
- const result = await synthesizer.synthesize({
33025
- app: appCmd,
33026
- output: options.output || "cdk.out",
33027
- ...Object.keys(context).length > 0 && { context }
33028
- });
33029
- appStacks = result.stacks.map((s) => ({
33030
- stackName: s.stackName,
33031
- displayName: s.displayName,
33032
- ...s.region && { region: s.region }
33033
- }));
33034
- } catch {
33035
- logger.debug("Could not synthesize app, falling back to state-based stack list");
33036
- }
33037
- }
33038
- const allStateRefs = await stateBackend.listStacks();
33039
- let candidateStacks;
33040
- if (appStacks.length > 0) {
33041
- const stateNames = new Set(allStateRefs.map((r) => r.stackName));
33042
- candidateStacks = appStacks.filter((s) => stateNames.has(s.stackName));
33043
- } else if (stackArgs.length > 0 || options.stack || options.all) {
33044
- const seen = /* @__PURE__ */ new Set();
33045
- candidateStacks = [];
33046
- for (const ref of allStateRefs) {
33047
- if (seen.has(ref.stackName))
33048
- continue;
33049
- seen.add(ref.stackName);
33050
- candidateStacks.push({ stackName: ref.stackName });
33051
- }
33052
- } else {
33053
- throw new Error(
33054
- "Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured."
33055
- );
33056
- }
33057
- const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
33058
- let stackNames;
33059
- if (options.all) {
33060
- stackNames = candidateStacks.map((s) => s.stackName);
33061
- } else if (stackPatterns.length > 0) {
33062
- stackNames = matchStacks(candidateStacks, stackPatterns).map((s) => s.stackName);
33063
- } else if (candidateStacks.length === 1) {
33064
- stackNames = candidateStacks.map((s) => s.stackName);
33065
- } else if (candidateStacks.length === 0) {
33066
- logger.info("No stacks found in state");
33067
- return;
33068
- } else {
33506
+ if (!appCmd) {
33069
33507
  throw new Error(
33070
- `Multiple stacks found: ${candidateStacks.map(describeStack).join(", ")}. Specify stack name(s) or use --all`
33508
+ "'cdkd orphan' requires a CDK app: pass --app or set it in cdk.json. The template is read to resolve construct paths to logical IDs."
33071
33509
  );
33072
33510
  }
33073
- if (stackNames.length === 0) {
33074
- logger.info("No matching stacks found in state");
33075
- return;
33076
- }
33077
- logger.info(`Found ${stackNames.length} stack(s) to orphan: ${stackNames.join(", ")}`);
33078
- const stateRefsByName = /* @__PURE__ */ new Map();
33079
- for (const ref of allStateRefs) {
33080
- const arr = stateRefsByName.get(ref.stackName) ?? [];
33081
- arr.push(ref);
33082
- stateRefsByName.set(ref.stackName, arr);
33511
+ logger.info("Synthesizing CDK app to read template...");
33512
+ const synthesizer = new Synthesizer();
33513
+ const context = parseContextOptions(options.context);
33514
+ const result = await synthesizer.synthesize({
33515
+ app: appCmd,
33516
+ output: options.output || "cdk.out",
33517
+ ...Object.keys(context).length > 0 && { context }
33518
+ });
33519
+ const resolved = resolveConstructPaths(pathArgs, result.stacks);
33520
+ const stackInfo = resolved.stack;
33521
+ const orphanLogicalIds = resolved.logicalIds;
33522
+ const targetRegion = await pickStackRegion(
33523
+ stateBackend,
33524
+ stackInfo.stackName,
33525
+ stackInfo.region,
33526
+ options.stackRegion
33527
+ );
33528
+ logger.info(
33529
+ `Target: ${stackInfo.stackName} (${targetRegion}); orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`
33530
+ );
33531
+ const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
33532
+ if (!options.dryRun) {
33533
+ await lockManager.acquireLock(stackInfo.stackName, targetRegion, owner, "orphan");
33083
33534
  }
33084
- const skipConfirmation = options.yes || options.force;
33085
- for (const stackName of stackNames) {
33086
- const refs = stateRefsByName.get(stackName) ?? [];
33087
- if (refs.length === 0) {
33088
- logger.info(`No state found for stack: ${stackName}, skipping`);
33089
- continue;
33535
+ try {
33536
+ const stateData = await stateBackend.getState(stackInfo.stackName, targetRegion);
33537
+ if (!stateData) {
33538
+ throw new Error(
33539
+ `No state found for stack '${stackInfo.stackName}' (${targetRegion}). Nothing to orphan. (Did the stack get deployed?)`
33540
+ );
33090
33541
  }
33091
- const targets = options.stackRegion ? refs.filter((r) => r.region === options.stackRegion) : refs;
33092
- if (targets.length === 0) {
33093
- const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
33542
+ const { state, etag, migrationPending } = stateData;
33543
+ const missing = orphanLogicalIds.filter((id) => !(id in state.resources));
33544
+ if (missing.length > 0) {
33545
+ const have = Object.keys(state.resources).join(", ");
33094
33546
  throw new Error(
33095
- `No state found for stack '${stackName}' in region '${options.stackRegion}'. Available regions: ${seen}.`
33547
+ `Resource(s) not in state for stack '${stackInfo.stackName}' (${targetRegion}): ${missing.join(", ")}.
33548
+ Available logical IDs: ${have}`
33096
33549
  );
33097
33550
  }
33098
- if (!options.force) {
33099
- for (const target of targets) {
33100
- const locked = await lockManager.isLocked(stackName, target.region);
33101
- if (locked) {
33102
- const where = target.region ?? "(legacy)";
33103
- throw new Error(
33104
- `Stack '${stackName}' (${where}) is locked. Run 'cdkd force-unlock ${stackName}${target.region ? ` --stack-region ${target.region}` : ""}' first, or pass --force to orphan anyway.`
33105
- );
33106
- }
33107
- }
33551
+ const providerRegistry = new ProviderRegistry();
33552
+ registerAllProviders(providerRegistry);
33553
+ const rewriteResult = await rewriteResourceReferences(
33554
+ state,
33555
+ orphanLogicalIds,
33556
+ providerRegistry,
33557
+ { force: options.force }
33558
+ );
33559
+ printRewriteSummary(rewriteResult.rewrites, orphanLogicalIds);
33560
+ if (rewriteResult.unresolvable.length > 0 && !options.force) {
33561
+ printUnresolvable(rewriteResult.unresolvable);
33562
+ throw new Error(
33563
+ `Orphan aborted: ${rewriteResult.unresolvable.length} reference(s) could not be resolved.
33564
+ Re-run with --force to fall back to cached attribute values from state, or fix the underlying provider/AWS issue and retry.`
33565
+ );
33108
33566
  }
33109
- if (!skipConfirmation) {
33110
- const targetList = targets.map((t) => t.region ? `${stackName} (${t.region})` : stackName).join(", ");
33111
- process.stdout.write(
33112
- `
33113
- WARNING: This removes cdkd's state record for [${targetList}] only. AWS resources will NOT be deleted.
33114
- Use 'cdkd destroy ${stackName}' if you want to delete the actual resources.
33115
-
33116
- `
33567
+ if (rewriteResult.unresolvable.length > 0) {
33568
+ printUnresolvable(rewriteResult.unresolvable);
33569
+ logger.warn(
33570
+ `--force: continuing despite ${rewriteResult.unresolvable.length} unresolved reference(s); the original intrinsic was left in place where the cache also lacked the value.`
33117
33571
  );
33118
- const rl = readline2.createInterface({
33119
- input: process.stdin,
33120
- output: process.stdout
33121
- });
33122
- const answer = await rl.question(
33123
- `Orphan state for ${targetList} from s3://${stateBucket}/${options.statePrefix}/? (y/N): `
33572
+ }
33573
+ if (options.dryRun) {
33574
+ logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
33575
+ return;
33576
+ }
33577
+ if (!options.yes && !options.force) {
33578
+ const ok = await confirmPrompt(
33579
+ `Orphan ${orphanLogicalIds.length} resource(s) from cdkd state for ${stackInfo.stackName} (${targetRegion})? AWS resources will NOT be deleted.`
33124
33580
  );
33125
- rl.close();
33126
- const trimmed = answer.trim().toLowerCase();
33127
- if (trimmed !== "y" && trimmed !== "yes") {
33128
- logger.info(`Cancelled orphan of stack: ${stackName}`);
33129
- continue;
33581
+ if (!ok) {
33582
+ logger.info("Orphan cancelled.");
33583
+ return;
33130
33584
  }
33131
33585
  }
33132
- for (const target of targets) {
33133
- if (target.region) {
33134
- await stateBackend.deleteState(stackName, target.region);
33135
- await lockManager.forceReleaseLock(stackName, target.region);
33136
- } else {
33137
- await lockManager.forceReleaseLock(stackName, void 0);
33138
- }
33139
- const label = target.region ? `${stackName} (${target.region})` : stackName;
33140
- logger.info(`\u2713 Orphaned state for stack: ${label}`);
33586
+ await stateBackend.saveState(stackInfo.stackName, targetRegion, rewriteResult.state, {
33587
+ expectedEtag: etag,
33588
+ ...migrationPending && { migrateLegacy: true }
33589
+ });
33590
+ logger.info(
33591
+ `Orphaned ${orphanLogicalIds.length} resource(s) from state: ${stackInfo.stackName} (${targetRegion}). AWS resources are still in AWS; cdkd will no longer manage them.`
33592
+ );
33593
+ } finally {
33594
+ if (!options.dryRun) {
33595
+ await lockManager.releaseLock(stackInfo.stackName, targetRegion).catch((err) => {
33596
+ logger.warn(
33597
+ `Failed to release lock: ${err instanceof Error ? err.message : String(err)}`
33598
+ );
33599
+ });
33141
33600
  }
33142
33601
  }
33143
33602
  } finally {
33144
33603
  awsClients.destroy();
33145
33604
  }
33146
33605
  }
33606
+ function resolveConstructPaths(paths, stacks) {
33607
+ const byStackName = /* @__PURE__ */ new Map();
33608
+ const byDisplayName = /* @__PURE__ */ new Map();
33609
+ for (const s of stacks) {
33610
+ byStackName.set(s.stackName, s);
33611
+ byDisplayName.set(s.displayName, s);
33612
+ }
33613
+ let stack;
33614
+ const logicalIds = [];
33615
+ for (const p of paths) {
33616
+ const slash = p.indexOf("/");
33617
+ if (slash <= 0 || slash === p.length - 1) {
33618
+ throw new Error(`Invalid construct path '${p}'. Expected '<StackName>/<Path/To/Resource>'.`);
33619
+ }
33620
+ const head = p.slice(0, slash);
33621
+ const candidate = byDisplayName.get(head) ?? byStackName.get(head);
33622
+ if (!candidate) {
33623
+ const available = stacks.map((s) => s.displayName ?? s.stackName).join(", ");
33624
+ throw new Error(
33625
+ `Construct path '${p}': stack '${head}' not found in synthesized app. Available: ${available}`
33626
+ );
33627
+ }
33628
+ if (stack === void 0) {
33629
+ stack = candidate;
33630
+ } else if (stack.stackName !== candidate.stackName) {
33631
+ throw new Error(
33632
+ `All construct paths must reference the same stack. Got '${stack.stackName}' and '${candidate.stackName}'. Run 'cdkd orphan' once per stack.`
33633
+ );
33634
+ }
33635
+ const cdkPath = p;
33636
+ const index = buildCdkPathIndex(candidate.template);
33637
+ const logicalId = index.get(cdkPath);
33638
+ if (!logicalId) {
33639
+ const available = [...index.keys()].sort().join("\n ");
33640
+ throw new Error(
33641
+ `Construct path '${cdkPath}' not found in template for stack '${candidate.stackName}'.
33642
+ Available paths:
33643
+ ${available}`
33644
+ );
33645
+ }
33646
+ if (!logicalIds.includes(logicalId)) {
33647
+ logicalIds.push(logicalId);
33648
+ }
33649
+ }
33650
+ if (!stack) {
33651
+ throw new Error("No construct paths supplied.");
33652
+ }
33653
+ return { stack, logicalIds };
33654
+ }
33655
+ async function pickStackRegion(stateBackend, stackName, synthRegion, flag) {
33656
+ const refs = (await stateBackend.listStacks()).filter((r) => r.stackName === stackName);
33657
+ if (refs.length === 0) {
33658
+ if (flag)
33659
+ return flag;
33660
+ if (synthRegion)
33661
+ return synthRegion;
33662
+ throw new Error(
33663
+ `No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
33664
+ );
33665
+ }
33666
+ if (flag) {
33667
+ const found = refs.find((r) => r.region === flag);
33668
+ if (!found) {
33669
+ const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
33670
+ throw new Error(
33671
+ `No state found for stack '${stackName}' in region '${flag}'. Available regions: ${seen}.`
33672
+ );
33673
+ }
33674
+ return flag;
33675
+ }
33676
+ if (synthRegion) {
33677
+ const found = refs.find((r) => r.region === synthRegion);
33678
+ if (found)
33679
+ return synthRegion;
33680
+ }
33681
+ if (refs.length === 1) {
33682
+ return refs[0].region ?? synthRegion ?? "";
33683
+ }
33684
+ const regions = refs.map((r) => r.region ?? "(legacy)").join(", ");
33685
+ throw new Error(
33686
+ `Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`
33687
+ );
33688
+ }
33689
+ function printRewriteSummary(rewrites, orphanLogicalIds) {
33690
+ const logger = getLogger();
33691
+ logger.info("");
33692
+ logger.info(`Orphaning ${orphanLogicalIds.length} resource(s): ${orphanLogicalIds.join(", ")}`);
33693
+ if (rewrites.length === 0) {
33694
+ logger.info(" No sibling references \u2014 every reference was already to a non-orphan resource.");
33695
+ return;
33696
+ }
33697
+ logger.info(`Applied ${rewrites.length} rewrite(s):`);
33698
+ for (const r of rewrites) {
33699
+ const before = stringifyForAudit(r.before);
33700
+ const after = r.kind === "dependency" ? "(dropped)" : stringifyForAudit(r.after);
33701
+ logger.info(` [${r.kind}] ${r.logicalId}.${r.path}: ${before} \u2192 ${after}`);
33702
+ }
33703
+ }
33704
+ function printUnresolvable(unresolvable) {
33705
+ const logger = getLogger();
33706
+ logger.error(`${unresolvable.length} reference(s) could not be resolved:`);
33707
+ for (const u of unresolvable) {
33708
+ logger.error(` ${u.logicalId}.${u.path}: ${u.orphanLogicalId}.${u.attribute} \u2014 ${u.reason}`);
33709
+ }
33710
+ }
33711
+ function stringifyForAudit(value) {
33712
+ if (typeof value === "string")
33713
+ return JSON.stringify(value);
33714
+ return JSON.stringify(value);
33715
+ }
33716
+ async function confirmPrompt(prompt) {
33717
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
33718
+ try {
33719
+ const ans = await rl.question(`${prompt} [y/N] `);
33720
+ return /^y(es)?$/i.test(ans.trim());
33721
+ } finally {
33722
+ rl.close();
33723
+ }
33724
+ }
33147
33725
  function createOrphanCommand() {
33148
33726
  const cmd = new Command7("orphan").description(
33149
- "Remove cdkd's state record for one or more stacks (does NOT delete AWS resources). Synth-driven; for the CDK-app-free version use 'cdkd state orphan'."
33727
+ "Remove one or more resources from cdkd state by construct path (does NOT delete AWS resources). Mirrors aws-cdk-cli's 'cdk orphan --unstable=orphan'. Synth-driven; for the previous whole-stack-orphan behavior, use 'cdkd state orphan <stack>'."
33150
33728
  ).argument(
33151
- "[stacks...]",
33152
- "Stack name(s) to orphan. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*')."
33153
- ).option("--all", "Orphan all stacks in the current app", false).option(
33729
+ "<paths...>",
33730
+ "Construct paths to orphan, e.g. 'MyStack/MyTable'. Multiple paths must reference the same stack."
33731
+ ).option(
33154
33732
  "--stack-region <region>",
33155
33733
  "Region of the stack record to operate on. Required when the same stack name has state in multiple regions."
33734
+ ).option(
33735
+ "--dry-run",
33736
+ "Compute and print the rewrite audit table without acquiring a lock or saving state.",
33737
+ false
33156
33738
  ).action(withErrorHandling(orphanCommand));
33157
33739
  [
33158
33740
  ...commonOptions,
33159
33741
  ...appOptions,
33160
33742
  ...stateOptions,
33161
- ...stackOptions,
33162
33743
  ...destroyOptions,
33744
+ // adds -f / --force (escape hatch for unresolvable references + skip confirm)
33163
33745
  ...contextOptions
33164
33746
  ].forEach((opt) => cmd.addOption(opt));
33165
33747
  cmd.addOption(deprecatedRegionOption);
@@ -33351,7 +33933,7 @@ async function stateMigrateCommand(options) {
33351
33933
  }
33352
33934
  if (!options.yes) {
33353
33935
  const action = options.removeLegacy ? "and DELETE the source bucket" : "(source bucket will be kept)";
33354
- const ok = await confirmPrompt(
33936
+ const ok = await confirmPrompt2(
33355
33937
  `Copy ${sourceObjects.length} object(s) from ${legacyBucket} -> ${newBucket} ${action}?`
33356
33938
  );
33357
33939
  if (!ok) {
@@ -33556,7 +34138,7 @@ async function emptyBucketAllVersions(s3, bucket) {
33556
34138
  versionIdMarker = resp.NextVersionIdMarker;
33557
34139
  } while (keyMarker || versionIdMarker);
33558
34140
  }
33559
- async function confirmPrompt(prompt) {
34141
+ async function confirmPrompt2(prompt) {
33560
34142
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
33561
34143
  try {
33562
34144
  const ans = await rl.question(`${prompt} [y/N] `);
@@ -34013,8 +34595,8 @@ async function stateDestroyCommand(stackArgs, options) {
34013
34595
  process.env["CDKD_NO_LIVE"] = "1";
34014
34596
  }
34015
34597
  validateResourceTimeouts({
34016
- resourceWarnAfter: options.resourceWarnAfter,
34017
- resourceTimeout: options.resourceTimeout
34598
+ ...options.resourceWarnAfter && { resourceWarnAfter: options.resourceWarnAfter },
34599
+ ...options.resourceTimeout && { resourceTimeout: options.resourceTimeout }
34018
34600
  });
34019
34601
  if (!options.all && stackArgs.length === 0) {
34020
34602
  throw new Error(
@@ -34116,8 +34698,18 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
34116
34698
  // skipped when `options.yes` is set OR `--all` was set (the user
34117
34699
  // already accepted the batch prompt).
34118
34700
  skipConfirmation: options.yes || options.all === true,
34119
- resourceWarnAfterMs: options.resourceWarnAfter,
34120
- resourceTimeoutMs: options.resourceTimeout
34701
+ ...options.resourceWarnAfter?.globalMs !== void 0 && {
34702
+ resourceWarnAfterMs: options.resourceWarnAfter.globalMs
34703
+ },
34704
+ ...options.resourceTimeout?.globalMs !== void 0 && {
34705
+ resourceTimeoutMs: options.resourceTimeout.globalMs
34706
+ },
34707
+ ...options.resourceWarnAfter?.perTypeMs && {
34708
+ resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs
34709
+ },
34710
+ ...options.resourceTimeout?.perTypeMs && {
34711
+ resourceTimeoutByType: options.resourceTimeout.perTypeMs
34712
+ }
34121
34713
  });
34122
34714
  totalErrors += result.errorCount;
34123
34715
  }
@@ -34285,7 +34877,7 @@ function createStateCommand() {
34285
34877
  }
34286
34878
 
34287
34879
  // src/cli/commands/import.ts
34288
- import { readFileSync as readFileSync5 } from "node:fs";
34880
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
34289
34881
  import * as readline5 from "node:readline/promises";
34290
34882
  import { Command as Command12 } from "commander";
34291
34883
  init_aws_clients();
@@ -34404,6 +34996,9 @@ async function importCommand(stackArg, options) {
34404
34996
  rows.push(outcome);
34405
34997
  }
34406
34998
  printSummary(rows);
34999
+ if (options.recordResourceMapping) {
35000
+ writeRecordedMapping(options.recordResourceMapping, rows);
35001
+ }
34407
35002
  if (options.dryRun) {
34408
35003
  logger.info("--dry-run: state will NOT be written. Re-run without --dry-run to apply.");
34409
35004
  return;
@@ -34414,7 +35009,7 @@ async function importCommand(stackArg, options) {
34414
35009
  return;
34415
35010
  }
34416
35011
  if (!options.yes) {
34417
- const ok = await confirmPrompt2(
35012
+ const ok = await confirmPrompt3(
34418
35013
  `Write state for ${stackInfo.stackName} (${targetRegion}) with ${importedRows.length} resource(s)?`
34419
35014
  );
34420
35015
  if (!ok) {
@@ -34557,12 +35152,24 @@ function parseMappingJson(raw, source) {
34557
35152
  }
34558
35153
  return out;
34559
35154
  }
34560
- function readCdkPath(resource) {
34561
- const meta = resource.Metadata;
34562
- if (!meta)
34563
- return "";
34564
- const v = meta["aws:cdk:path"];
34565
- return typeof v === "string" ? v : "";
35155
+ function writeRecordedMapping(filePath, rows) {
35156
+ const logger = getLogger();
35157
+ const map = {};
35158
+ for (const row of rows) {
35159
+ if (row.outcome === "imported" && row.physicalId) {
35160
+ map[row.logicalId] = row.physicalId;
35161
+ }
35162
+ }
35163
+ const body = JSON.stringify(map, null, 2) + "\n";
35164
+ try {
35165
+ writeFileSync4(filePath, body, "utf-8");
35166
+ logger.info(`Wrote resolved mapping to ${filePath} (${Object.keys(map).length} entry(ies))`);
35167
+ } catch (err) {
35168
+ const msg = err instanceof Error ? err.message : String(err);
35169
+ logger.error(
35170
+ `Failed to write --record-resource-mapping file '${filePath}': ${msg}. Continuing \u2014 the import already resolved every physical id in memory.`
35171
+ );
35172
+ }
34566
35173
  }
34567
35174
  function collectImportableResources(template) {
34568
35175
  const out = [];
@@ -34635,7 +35242,7 @@ function formatOutcome(outcome) {
34635
35242
  return "\u2717";
34636
35243
  }
34637
35244
  }
34638
- async function confirmPrompt2(prompt) {
35245
+ async function confirmPrompt3(prompt) {
34639
35246
  const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
34640
35247
  try {
34641
35248
  const ans = await rl.question(`${prompt} [y/N] `);
@@ -34661,6 +35268,9 @@ function createImportCommand() {
34661
35268
  ).option(
34662
35269
  "--resource-mapping-inline <json>",
34663
35270
  "Inline JSON object of {logicalId: physicalId} overrides (CDK CLI `cdk import --resource-mapping-inline` compatible). Same shape as --resource-mapping but supplied as a string \u2014 useful for non-TTY CI scripts that do not want a separate file. Implies selective mode unless --auto is set. Mutually exclusive with --resource-mapping."
35271
+ ).option(
35272
+ "--record-resource-mapping <file>",
35273
+ 'After cdkd resolves every logical ID (via --resource / --resource-mapping / tag-based auto-lookup), write the resulting {logicalId: physicalId} map to <file> as JSON. Useful in auto / hybrid mode for capturing the tag-resolved mapping and feeding it back as --resource-mapping in non-interactive CI re-runs. Written before the confirmation prompt (so the user can review the file before saying "yes") and even when the user says "no". Mirrors `cdk import --record-resource-mapping`.'
34664
35274
  ).option(
34665
35275
  "--auto",
34666
35276
  "Hybrid mode: when explicit overrides are supplied, ALSO tag-import every other resource in the template. Without this flag, --resource / --resource-mapping behave as a whitelist (CDK CLI parity).",
@@ -34706,7 +35316,7 @@ function reorderArgs(argv) {
34706
35316
  }
34707
35317
  async function main() {
34708
35318
  const program = new Command13();
34709
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.25.0");
35319
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.26.0");
34710
35320
  program.addCommand(createBootstrapCommand());
34711
35321
  program.addCommand(createSynthCommand());
34712
35322
  program.addCommand(createListCommand());