@go-to-k/cdkd 0.47.0 → 0.49.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/README.md CHANGED
@@ -171,6 +171,57 @@ the full per-type table.
171
171
  | CC API null value stripping | ✅ | Removes null values before API calls |
172
172
  | Retry with HTTP status codes | ✅ | 429/503 + cause chain inspection |
173
173
 
174
+ ### Drift detection
175
+
176
+ `cdkd drift <stack>` (state-driven; no synth) compares each resource
177
+ between the AWS-current snapshot returned by `provider.readCurrentState`
178
+ and the **deploy-time AWS snapshot** stored in
179
+ `ResourceState.observedProperties`. The observedProperties baseline is
180
+ populated automatically on every successful `cdkd deploy` /
181
+ `cdkd import`, so console-side changes to keys you did NOT template
182
+ (IAM policies attached out-of-band, S3 public-access-block toggled,
183
+ etc.) surface as drift instead of being silently ignored.
184
+
185
+ State schema `version: 3` (the layout that carries observedProperties)
186
+ is auto-migrated on the next write — no user action needed for new
187
+ deploys. **For stacks already deployed with an older binary**, the
188
+ upgrade story is:
189
+
190
+ 1. `cdkd 0.46.x` (or earlier) deployed your stack — state.json is
191
+ `version: 2`, no observedProperties on any resource.
192
+ 2. Upgrade cdkd to `0.47.0+`. The new binary reads v2 state cleanly,
193
+ and `cdkd drift` falls back to comparing against the user-templated
194
+ `properties` field (= the pre-v3 behavior) for any resource that
195
+ hasn't been refreshed yet.
196
+ 3. **Populate observedProperties** for the existing stack — pick one:
197
+
198
+ ```bash
199
+ # Option A (recommended): explicit refresh, no redeploy.
200
+ cdkd state refresh-observed MyStack
201
+
202
+ # Option B: trigger an UPDATE on each affected resource via the
203
+ # next `cdkd deploy`. NO_CHANGE-skipped resources are NOT refreshed
204
+ # by deploy alone — only the ones whose template changed get a
205
+ # readCurrentState call. Use this only if you're already changing
206
+ # the template; for an upgrade-and-refresh-only flow prefer A.
207
+ cdkd deploy MyStack
208
+ ```
209
+ 4. Re-run `cdkd drift MyStack` — now observed-baseline drift detection
210
+ is fully enabled.
211
+
212
+ `cdkd state refresh-observed --all` does the same for every stack in
213
+ the state bucket; `--dry-run` prints the per-stack refresh count
214
+ without touching state. Resolve any drift the comparator finds with
215
+ `cdkd drift <stack> --accept` (state ← AWS) or `--revert` (AWS ← state).
216
+
217
+ `cdkd deploy --no-capture-observed-state` (or
218
+ `cdk.json context.cdkd.captureObservedState: false`) opts out of the
219
+ capture entirely if you care more about deploy speed than rich drift
220
+ detection — drift then falls back to comparing against `properties`,
221
+ the pre-v3 behavior. Bench measurements show roughly +0–4% deploy
222
+ time with the capture on (lambda integ within noise; bench-cdk-sample
223
+ +3.4% median).
224
+
174
225
  ## Prerequisites
175
226
 
176
227
  - **Node.js** >= 20.0.0
@@ -300,6 +351,14 @@ cdkd drift MyStack --accept --yes
300
351
  # Resolve drift: AWS ← state (push state values back into AWS via provider.update)
301
352
  cdkd drift MyStack --revert --yes
302
353
 
354
+ # Refresh the deploy-time AWS snapshot used as drift baseline.
355
+ # Run this once after upgrading from a pre-v3 cdkd binary (= state schema
356
+ # `version: 2`) so console-side changes to keys you didn't template can
357
+ # be detected for resources that won't change in any near-future deploy.
358
+ # Same idempotent behavior on the same v3 state — see "Drift detection"
359
+ # below for the full upgrade story.
360
+ cdkd state refresh-observed MyStack
361
+
303
362
  # Dry run (plan only, no changes)
304
363
  cdkd deploy --dry-run
305
364
 
package/dist/cli.js CHANGED
@@ -9181,8 +9181,7 @@ var IAMRoleProvider = class {
9181
9181
  marker = tagsResp.Marker;
9182
9182
  }
9183
9183
  const tags = normalizeAwsTagsToCfn(collected);
9184
- if (tags.length > 0)
9185
- result["Tags"] = tags;
9184
+ result["Tags"] = tags;
9186
9185
  } catch (err) {
9187
9186
  if (!(err instanceof NoSuchEntityException))
9188
9187
  throw err;
@@ -12205,8 +12204,7 @@ var S3BucketProvider = class {
12205
12204
  try {
12206
12205
  const resp = await this.s3Client.send(new GetBucketTaggingCommand({ Bucket: physicalId }));
12207
12206
  const tags = normalizeAwsTagsToCfn(resp.TagSet);
12208
- if (tags.length > 0)
12209
- result["Tags"] = tags;
12207
+ result["Tags"] = tags;
12210
12208
  } catch (err) {
12211
12209
  const e = err;
12212
12210
  if (e.name !== "NoSuchTagSet") {
@@ -12892,8 +12890,7 @@ var SQSQueueProvider = class {
12892
12890
  new ListQueueTagsCommand({ QueueUrl: physicalId })
12893
12891
  );
12894
12892
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
12895
- if (tags.length > 0)
12896
- result["Tags"] = tags;
12893
+ result["Tags"] = tags;
12897
12894
  } catch (err) {
12898
12895
  if (err instanceof QueueDoesNotExist)
12899
12896
  return void 0;
@@ -13585,8 +13582,7 @@ var SNSTopicProvider = class {
13585
13582
  new ListTagsForResourceCommand({ ResourceArn: physicalId })
13586
13583
  );
13587
13584
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
13588
- if (tags.length > 0)
13589
- result["Tags"] = tags;
13585
+ result["Tags"] = tags;
13590
13586
  } catch (err) {
13591
13587
  if (err instanceof NotFoundException)
13592
13588
  return void 0;
@@ -14872,8 +14868,7 @@ var LambdaFunctionProvider = class {
14872
14868
  result["VpcConfig"] = vpc;
14873
14869
  }
14874
14870
  const tags = normalizeAwsTagsToCfn(resp.Tags);
14875
- if (tags.length > 0)
14876
- result["Tags"] = tags;
14871
+ result["Tags"] = tags;
14877
14872
  return result;
14878
14873
  } catch (err) {
14879
14874
  if (err instanceof ResourceNotFoundException)
@@ -16445,8 +16440,7 @@ var DynamoDBTableProvider = class {
16445
16440
  new ListTagsOfResourceCommand({ ResourceArn: table.TableArn })
16446
16441
  );
16447
16442
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
16448
- if (tags.length > 0)
16449
- result["Tags"] = tags;
16443
+ result["Tags"] = tags;
16450
16444
  } catch (err) {
16451
16445
  if (err instanceof ResourceNotFoundException6)
16452
16446
  return void 0;
@@ -16811,8 +16805,7 @@ var LogsLogGroupProvider = class {
16811
16805
  new ListTagsForResourceCommand2({ resourceArn: arnForTags })
16812
16806
  );
16813
16807
  const tags = normalizeAwsTagsToCfn(tagsResp.tags);
16814
- if (tags.length > 0)
16815
- result["Tags"] = tags;
16808
+ result["Tags"] = tags;
16816
16809
  } catch (err) {
16817
16810
  if (err instanceof ResourceNotFoundException7)
16818
16811
  return void 0;
@@ -17194,8 +17187,7 @@ var CloudWatchAlarmProvider = class {
17194
17187
  new ListTagsForResourceCommand3({ ResourceARN: alarm.AlarmArn })
17195
17188
  );
17196
17189
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
17197
- if (tags.length > 0)
17198
- result["Tags"] = tags;
17190
+ result["Tags"] = tags;
17199
17191
  } catch (err) {
17200
17192
  this.logger.debug(
17201
17193
  `CloudWatch ListTagsForResource(${alarm.AlarmArn}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -17569,8 +17561,7 @@ var SecretsManagerSecretProvider = class {
17569
17561
  });
17570
17562
  }
17571
17563
  const tags = normalizeAwsTagsToCfn(resp.Tags);
17572
- if (tags.length > 0)
17573
- result["Tags"] = tags;
17564
+ result["Tags"] = tags;
17574
17565
  return result;
17575
17566
  } catch (err) {
17576
17567
  if (err instanceof ResourceNotFoundException8)
@@ -17932,8 +17923,7 @@ var SSMParameterProvider = class {
17932
17923
  })
17933
17924
  );
17934
17925
  const tags = normalizeAwsTagsToCfn(tagsResp.TagList);
17935
- if (tags.length > 0)
17936
- result["Tags"] = tags;
17926
+ result["Tags"] = tags;
17937
17927
  } catch {
17938
17928
  }
17939
17929
  return result;
@@ -18345,8 +18335,7 @@ var EventBridgeRuleProvider = class {
18345
18335
  new ListTagsForResourceCommand5({ ResourceARN: physicalId })
18346
18336
  );
18347
18337
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
18348
- if (tags.length > 0)
18349
- result["Tags"] = tags;
18338
+ result["Tags"] = tags;
18350
18339
  } catch (err) {
18351
18340
  if (!(err instanceof ResourceNotFoundException9)) {
18352
18341
  throw err;
@@ -18762,8 +18751,7 @@ var EventBridgeBusProvider = class {
18762
18751
  new ListTagsForResourceCommand6({ ResourceARN: resp.Arn })
18763
18752
  );
18764
18753
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
18765
- if (tags.length > 0)
18766
- result["Tags"] = tags;
18754
+ result["Tags"] = tags;
18767
18755
  } catch (err) {
18768
18756
  if (err instanceof ResourceNotFoundException10)
18769
18757
  return void 0;
@@ -23069,8 +23057,7 @@ var ApiGatewayV2Provider = class {
23069
23057
  if (resp.CorsConfiguration)
23070
23058
  result["CorsConfiguration"] = resp.CorsConfiguration;
23071
23059
  const tags = normalizeAwsTagsToCfn(resp.Tags);
23072
- if (tags.length > 0)
23073
- result["Tags"] = tags;
23060
+ result["Tags"] = tags;
23074
23061
  return result;
23075
23062
  } catch (err) {
23076
23063
  if (err instanceof NotFoundException4)
@@ -24596,8 +24583,7 @@ var StepFunctionsProvider = class {
24596
24583
  new ListTagsForResourceCommand8({ resourceArn: physicalId })
24597
24584
  );
24598
24585
  const tags = normalizeAwsTagsToCfn(tagsResp.tags);
24599
- if (tags.length > 0)
24600
- result["Tags"] = tags;
24586
+ result["Tags"] = tags;
24601
24587
  } catch (err) {
24602
24588
  if (!(err instanceof StateMachineDoesNotExist))
24603
24589
  throw err;
@@ -25489,8 +25475,7 @@ var ECSProvider = class {
25489
25475
  }));
25490
25476
  }
25491
25477
  const tags = normalizeAwsTagsToCfn(c.tags);
25492
- if (tags.length > 0)
25493
- result["Tags"] = tags;
25478
+ result["Tags"] = tags;
25494
25479
  return result;
25495
25480
  }
25496
25481
  async readCurrentStateService(physicalId) {
@@ -25560,8 +25545,7 @@ var ECSProvider = class {
25560
25545
  result["ServiceRegistries"] = s.serviceRegistries;
25561
25546
  }
25562
25547
  const tags = normalizeAwsTagsToCfn(s.tags);
25563
- if (tags.length > 0)
25564
- result["Tags"] = tags;
25548
+ result["Tags"] = tags;
25565
25549
  return result;
25566
25550
  }
25567
25551
  async readCurrentStateTaskDefinition(physicalId) {
@@ -25612,8 +25596,7 @@ var ECSProvider = class {
25612
25596
  result["ContainerDefinitions"] = td.containerDefinitions;
25613
25597
  }
25614
25598
  const tags = normalizeAwsTagsToCfn(resp.tags);
25615
- if (tags.length > 0)
25616
- result["Tags"] = tags;
25599
+ result["Tags"] = tags;
25617
25600
  return result;
25618
25601
  }
25619
25602
  /**
@@ -26403,8 +26386,7 @@ var ELBv2Provider = class {
26403
26386
  const resp = await this.getClient().send(new DescribeTagsCommand({ ResourceArns: [arn] }));
26404
26387
  const tagDesc = resp.TagDescriptions?.[0];
26405
26388
  const tags = normalizeAwsTagsToCfn(tagDesc?.Tags);
26406
- if (tags.length > 0)
26407
- result["Tags"] = tags;
26389
+ result["Tags"] = tags;
26408
26390
  } catch (err) {
26409
26391
  this.logger.debug(
26410
26392
  `ELBv2 DescribeTags(${arn}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -27316,8 +27298,7 @@ var RDSProvider = class {
27316
27298
  new ListTagsForResourceCommand10({ ResourceName: arn })
27317
27299
  );
27318
27300
  const tags = normalizeAwsTagsToCfn(tagsResp.TagList);
27319
- if (tags.length > 0)
27320
- result["Tags"] = tags;
27301
+ result["Tags"] = tags;
27321
27302
  } catch (err) {
27322
27303
  this.logger.debug(
27323
27304
  `RDS ListTagsForResource(${arn}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -28615,8 +28596,7 @@ var WAFv2WebACLProvider = class {
28615
28596
  new ListTagsForResourceCommand12({ ResourceARN: physicalId })
28616
28597
  );
28617
28598
  const tags = normalizeAwsTagsToCfn(tagsResp.TagInfoForResource?.TagList);
28618
- if (tags.length > 0)
28619
- result["Tags"] = tags;
28599
+ result["Tags"] = tags;
28620
28600
  } catch (err) {
28621
28601
  this.logger.debug(
28622
28602
  `WAFv2 ListTagsForResource(${physicalId}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -29747,8 +29727,7 @@ var ElastiCacheProvider = class {
29747
29727
  new ListTagsForResourceCommand14({ ResourceName: arn })
29748
29728
  );
29749
29729
  const tags = normalizeAwsTagsToCfn(tagsResp.TagList);
29750
- if (tags.length > 0)
29751
- result["Tags"] = tags;
29730
+ result["Tags"] = tags;
29752
29731
  } catch (err) {
29753
29732
  this.logger.debug(
29754
29733
  `ElastiCache ListTagsForResource(${arn}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -30321,8 +30300,7 @@ var ServiceDiscoveryProvider = class {
30321
30300
  new ListTagsForResourceCommand15({ ResourceARN: arn })
30322
30301
  );
30323
30302
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
30324
- if (tags.length > 0)
30325
- result["Tags"] = tags;
30303
+ result["Tags"] = tags;
30326
30304
  } catch (err) {
30327
30305
  this.logger.debug(
30328
30306
  `ServiceDiscovery ListTagsForResource(${arn}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -31046,8 +31024,7 @@ var AppSyncProvider = class {
31046
31024
  result["LogConfig"] = log;
31047
31025
  }
31048
31026
  const tags = normalizeAwsTagsToCfn(api.tags);
31049
- if (tags.length > 0)
31050
- result["Tags"] = tags;
31027
+ result["Tags"] = tags;
31051
31028
  return result;
31052
31029
  }
31053
31030
  async readDataSource(physicalId) {
@@ -32346,8 +32323,7 @@ var KMSProvider = class {
32346
32323
  new ListResourceTagsCommand({ KeyId: md.KeyId })
32347
32324
  );
32348
32325
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
32349
- if (tags.length > 0)
32350
- result["Tags"] = tags;
32326
+ result["Tags"] = tags;
32351
32327
  } catch (err) {
32352
32328
  if (err instanceof NotFoundException5)
32353
32329
  return void 0;
@@ -32791,8 +32767,7 @@ var KinesisStreamProvider = class {
32791
32767
  new ListTagsForStreamCommand({ StreamName: physicalId })
32792
32768
  );
32793
32769
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
32794
- if (tags.length > 0)
32795
- result["Tags"] = tags;
32770
+ result["Tags"] = tags;
32796
32771
  } catch (err) {
32797
32772
  if (err instanceof ResourceNotFoundException13)
32798
32773
  return void 0;
@@ -33992,8 +33967,7 @@ var FirehoseProvider = class {
33992
33967
  new ListTagsForDeliveryStreamCommand({ DeliveryStreamName: physicalId })
33993
33968
  );
33994
33969
  const tags = normalizeAwsTagsToCfn(tagsResp.Tags);
33995
- if (tags.length > 0)
33996
- result["Tags"] = tags;
33970
+ result["Tags"] = tags;
33997
33971
  } catch (err) {
33998
33972
  if (err instanceof ResourceNotFoundException14)
33999
33973
  return void 0;
@@ -34410,8 +34384,7 @@ var CloudTrailProvider = class {
34410
34384
  new ListTagsCommand3({ ResourceIdList: [trail.TrailARN] })
34411
34385
  );
34412
34386
  const tags = normalizeAwsTagsToCfn(tagsResp.ResourceTagList?.[0]?.TagsList);
34413
- if (tags.length > 0)
34414
- result["Tags"] = tags;
34387
+ result["Tags"] = tags;
34415
34388
  } catch (err) {
34416
34389
  this.logger.debug(
34417
34390
  `CloudTrail ListTags(${trail.TrailARN}) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -34875,8 +34848,7 @@ var CodeBuildProvider = class {
34875
34848
  result["Environment"] = env;
34876
34849
  }
34877
34850
  const tags = normalizeAwsTagsToCfn(project.tags);
34878
- if (tags.length > 0)
34879
- result["Tags"] = tags;
34851
+ result["Tags"] = tags;
34880
34852
  return result;
34881
34853
  }
34882
34854
  async import(input) {
@@ -36471,8 +36443,7 @@ var ECRProvider = class {
36471
36443
  new ListTagsForResourceCommand18({ resourceArn: r.repositoryArn })
36472
36444
  );
36473
36445
  const tags = normalizeAwsTagsToCfn(tagsResp.tags);
36474
- if (tags.length > 0)
36475
- result["Tags"] = tags;
36446
+ result["Tags"] = tags;
36476
36447
  } catch (err) {
36477
36448
  if (!(err instanceof RepositoryNotFoundException))
36478
36449
  throw err;
@@ -39057,6 +39028,14 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
39057
39028
  resource.properties ?? {}
39058
39029
  );
39059
39030
  } else {
39031
+ if (resource.resourceType.startsWith("Custom::")) {
39032
+ outcomes.push({
39033
+ kind: "unsupported",
39034
+ logicalId,
39035
+ resourceType: resource.resourceType
39036
+ });
39037
+ continue;
39038
+ }
39060
39039
  if (CC_API_FALLBACK_DENY_LIST[resource.resourceType]) {
39061
39040
  outcomes.push({
39062
39041
  kind: "unsupported",
@@ -41875,6 +41854,215 @@ function createStateInfoCommand() {
41875
41854
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
41876
41855
  return cmd;
41877
41856
  }
41857
+ async function stateRefreshObservedCommand(stackArgs, options) {
41858
+ const logger = getLogger();
41859
+ if (options.verbose)
41860
+ logger.setLevel("debug");
41861
+ if (!options.all && stackArgs.length === 0) {
41862
+ throw new Error(
41863
+ "Stack name is required. Usage: cdkd state refresh-observed <stack> [<stack>...] | --all"
41864
+ );
41865
+ }
41866
+ const setup = await setupStateBackend(options);
41867
+ const providerRegistry = new ProviderRegistry();
41868
+ registerAllProviders(providerRegistry);
41869
+ providerRegistry.setCustomResourceResponseBucket(setup.bucket);
41870
+ try {
41871
+ const stateRefs = await setup.stateBackend.listStacks();
41872
+ let targets;
41873
+ if (options.all) {
41874
+ targets = options.stackRegion ? stateRefs.filter((r) => r.region === options.stackRegion) : stateRefs;
41875
+ if (targets.length === 0) {
41876
+ logger.info("No stacks found in state");
41877
+ return;
41878
+ }
41879
+ } else {
41880
+ targets = [];
41881
+ for (const stackName of stackArgs) {
41882
+ const matches = stateRefs.filter((r) => r.stackName === stackName);
41883
+ if (matches.length === 0) {
41884
+ throw new Error(
41885
+ `No state found for stack '${stackName}'. Run 'cdkd state list' to see available stacks.`
41886
+ );
41887
+ }
41888
+ if (options.stackRegion) {
41889
+ const ref = matches.find((r) => r.region === options.stackRegion);
41890
+ if (!ref) {
41891
+ const seen = matches.map((r) => r.region ?? "(legacy)").join(", ");
41892
+ throw new Error(
41893
+ `No state found for stack '${stackName}' in region '${options.stackRegion}'. Available regions: ${seen}.`
41894
+ );
41895
+ }
41896
+ targets.push(ref);
41897
+ } else if (matches.length === 1) {
41898
+ targets.push(matches[0]);
41899
+ } else {
41900
+ const regions = matches.map((r) => r.region ?? "(legacy)").join(", ");
41901
+ throw new Error(
41902
+ `Stack '${stackName}' has state in multiple regions: ${regions}. Re-run with --stack-region <region> to disambiguate.`
41903
+ );
41904
+ }
41905
+ }
41906
+ }
41907
+ if (!options.yes && !options.dryRun) {
41908
+ const targetList = targets.map(formatStackRef).join(", ");
41909
+ const ok = await confirmRefresh(
41910
+ `Refresh observedProperties for ${targets.length} stack(s) (${targetList})?`
41911
+ );
41912
+ if (!ok) {
41913
+ logger.info("Aborted.");
41914
+ return;
41915
+ }
41916
+ }
41917
+ let totalRefreshed = 0;
41918
+ let totalUnsupported = 0;
41919
+ let totalFailed = 0;
41920
+ for (const target of targets) {
41921
+ if (!target.region) {
41922
+ throw new Error(
41923
+ `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.`
41924
+ );
41925
+ }
41926
+ const counts = await refreshObservedForStack(
41927
+ target.stackName,
41928
+ target.region,
41929
+ setup.stateBackend,
41930
+ setup.lockManager,
41931
+ providerRegistry,
41932
+ { dryRun: options.dryRun ?? false, logger }
41933
+ );
41934
+ totalRefreshed += counts.refreshed;
41935
+ totalUnsupported += counts.unsupported;
41936
+ totalFailed += counts.failed;
41937
+ }
41938
+ const summary = options.dryRun ? `Plan: ${totalRefreshed} resource(s) would be refreshed, ${totalUnsupported} unsupported, ${totalFailed} would fail (--dry-run, no state was written)` : `Done: ${totalRefreshed} resource(s) refreshed, ${totalUnsupported} unsupported, ${totalFailed} failed`;
41939
+ logger.info(`
41940
+ ${summary}`);
41941
+ if (totalFailed > 0) {
41942
+ throw new PartialFailureError(
41943
+ `Refresh completed with ${totalFailed} per-resource readCurrentState failure(s). Affected resources keep their previous observedProperties (or no observedProperties at all). Re-run 'cdkd state refresh-observed' to retry.`
41944
+ );
41945
+ }
41946
+ } finally {
41947
+ setup.dispose();
41948
+ }
41949
+ }
41950
+ async function refreshObservedForStack(stackName, region, stateBackend, lockManager, providerRegistry, opts) {
41951
+ const { logger } = opts;
41952
+ const result = await stateBackend.getState(stackName, region);
41953
+ if (!result) {
41954
+ throw new Error(
41955
+ `No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`
41956
+ );
41957
+ }
41958
+ const { state, etag, migrationPending } = result;
41959
+ const entries = Object.entries(state.resources ?? {});
41960
+ if (entries.length === 0) {
41961
+ logger.info(`\u2713 ${stackName} (${region}): no resources in state, skipping`);
41962
+ return { refreshed: 0, unsupported: 0, failed: 0 };
41963
+ }
41964
+ if (opts.dryRun) {
41965
+ let wouldRefresh = 0;
41966
+ let wouldUnsupported = 0;
41967
+ for (const [, resource] of entries) {
41968
+ let provider;
41969
+ try {
41970
+ provider = providerRegistry.getProvider(resource.resourceType);
41971
+ } catch {
41972
+ wouldUnsupported++;
41973
+ continue;
41974
+ }
41975
+ if (provider.readCurrentState)
41976
+ wouldRefresh++;
41977
+ else
41978
+ wouldUnsupported++;
41979
+ }
41980
+ logger.info(
41981
+ `Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported`
41982
+ );
41983
+ return { refreshed: wouldRefresh, unsupported: wouldUnsupported, failed: 0 };
41984
+ }
41985
+ const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
41986
+ await lockManager.acquireLock(stackName, region, owner, "state-refresh-observed");
41987
+ try {
41988
+ let refreshed = 0;
41989
+ let unsupported = 0;
41990
+ let failed = 0;
41991
+ await withStackName(stackName, async () => {
41992
+ const tasks = entries.map(async ([logicalId, resource]) => {
41993
+ if (providerRegistry.shouldSkipResource(resource.resourceType)) {
41994
+ unsupported++;
41995
+ return;
41996
+ }
41997
+ let provider;
41998
+ try {
41999
+ provider = providerRegistry.getProvider(resource.resourceType);
42000
+ } catch {
42001
+ unsupported++;
42002
+ return;
42003
+ }
42004
+ if (!provider.readCurrentState) {
42005
+ unsupported++;
42006
+ return;
42007
+ }
42008
+ try {
42009
+ const observed = await provider.readCurrentState(
42010
+ resource.physicalId,
42011
+ logicalId,
42012
+ resource.resourceType,
42013
+ resource.properties ?? {}
42014
+ );
42015
+ if (observed === void 0) {
42016
+ unsupported++;
42017
+ return;
42018
+ }
42019
+ resource.observedProperties = observed;
42020
+ refreshed++;
42021
+ } catch (err) {
42022
+ failed++;
42023
+ logger.warn(
42024
+ ` \u2717 ${stackName}/${logicalId} (${resource.resourceType}): readCurrentState failed \u2014 ${err instanceof Error ? err.message : String(err)}`
42025
+ );
42026
+ }
42027
+ });
42028
+ await Promise.all(tasks);
42029
+ });
42030
+ state.lastModified = Date.now();
42031
+ const saveOptions = {
42032
+ expectedEtag: etag
42033
+ };
42034
+ if (migrationPending)
42035
+ saveOptions.migrateLegacy = true;
42036
+ await stateBackend.saveState(stackName, region, state, saveOptions);
42037
+ logger.info(
42038
+ `\u2713 ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed`
42039
+ );
42040
+ return { refreshed, unsupported, failed };
42041
+ } finally {
42042
+ await lockManager.releaseLock(stackName, region).catch((err) => {
42043
+ logger.warn(
42044
+ `Failed to release lock for ${stackName} (${region}): ` + (err instanceof Error ? err.message : String(err))
42045
+ );
42046
+ });
42047
+ }
42048
+ }
42049
+ async function confirmRefresh(prompt) {
42050
+ const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
42051
+ try {
42052
+ const ans = await rl.question(`${prompt} [y/N] `);
42053
+ return /^y(es)?$/i.test(ans.trim());
42054
+ } finally {
42055
+ rl.close();
42056
+ }
42057
+ }
42058
+ function createStateRefreshObservedCommand() {
42059
+ const cmd = new Command12("refresh-observed").description(
42060
+ "Refresh observedProperties for every resource in a stack by calling provider.readCurrentState \u2014 populates the drift baseline for stacks deployed before state schema v3, without redeploying."
42061
+ ).argument("[stacks...]", "Stack name(s) to refresh (physical CloudFormation names)").option("--all", "Refresh every stack in the state bucket", false).option("--dry-run", "Print the per-stack refresh count without writing state", false).addOption(stackRegionOption2()).action(withErrorHandling(stateRefreshObservedCommand));
42062
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
42063
+ cmd.addOption(deprecatedRegionOption);
42064
+ return cmd;
42065
+ }
41878
42066
  function createStateCommand() {
41879
42067
  const cmd = new Command12("state").description("Manage cdkd state stored in S3");
41880
42068
  cmd.addCommand(createStateInfoCommand());
@@ -41884,6 +42072,7 @@ function createStateCommand() {
41884
42072
  cmd.addCommand(createStateOrphanCommand());
41885
42073
  cmd.addCommand(createStateDestroyCommand());
41886
42074
  cmd.addCommand(createStateMigrateCommand());
42075
+ cmd.addCommand(createStateRefreshObservedCommand());
41887
42076
  return cmd;
41888
42077
  }
41889
42078
 
@@ -42666,7 +42855,7 @@ function reorderArgs(argv) {
42666
42855
  }
42667
42856
  async function main() {
42668
42857
  const program = new Command14();
42669
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.47.0");
42858
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.49.0");
42670
42859
  program.addCommand(createBootstrapCommand());
42671
42860
  program.addCommand(createSynthCommand());
42672
42861
  program.addCommand(createListCommand());