@go-to-k/cdkd 0.219.8 → 0.220.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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { A as withErrorHandling, B as generateResourceNameWithFallback, C as StackHasActiveImportsError, F as getLiveRenderer, H as withStackName, I as PATTERN_B_NAME_PROPERTIES, L as PATTERN_B_RESOURCE_TYPES, M as getLogger, P as runStackBuffered, S as ResourceUpdateNotSupportedError, V as withSkipPrefix, _ as MissingCdkCliError, a as assertRegionMatch, b as ProvisioningError, f as LocalInvokeBuildError$1, i as resolveExplicitPhysicalId, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as matchesCdkPath, o as disableInstanceApiTermination, p as LocalMigrateError, r as normalizeAwsTagsToCfn, s as isTerminationProtectionPropagationError, t as CDK_PATH_TAG, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError, z as generateResourceName } from "./import-helpers-wLipXr5g.js";
3
3
  import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-pjPwZz1r.js";
4
- import { A as WorkGraph, B as resolveCaptureObservedState, C as DagBuilder, D as shouldRetainResource, E as S3StateBackend, F as runDockerStreaming, G as CFN_TEMPLATE_BODY_LIMIT, H as resolveStateBucketWithDefault, I as Synthesizer, J as findLargeInlineResources, K as CFN_TEMPLATE_URL_LIMIT, L as getDefaultStateBucketName, M as formatDockerLoginError, N as getDockerCmd, O as AssetPublisher, P as runDockerForeground, Q as resolveBucketRegion, R as getLegacyStateBucketName, S as DiffCalculator, T as LockManager, U as resolveStateBucketWithDefaultAndSource, V as resolveSkipPrefix, W as warnDeprecatedNoPrefixCliFlag, X as AssemblyReader, Y as uploadCfnTemplate, _ as ProviderRegistry, a as withRetry, b as IntrinsicFunctionResolver, c as formatResourceLine, d as gray, f as green, g as collectInlinePolicyNamesManagedBySiblings, h as IAMRoleProvider, i as withResourceDeadline, j as buildDockerImage, k as stringifyValue, l as bold, m as yellow, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as red, q as MIGRATE_TMP_PREFIX, r as DeployEngine, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as cyan, v as findActionableSilentDrops, w as TemplateParser, x as applyRoleArnIfSet, y as CloudControlProvider, z as resolveApp } from "./deploy-engine-Bl8wyosc.js";
4
+ import { $ as resolveBucketRegion, A as stringifyValue, B as resolveApp, C as DiffCalculator, D as S3StateBackend, E as LockManager, F as runDockerForeground, G as warnDeprecatedNoPrefixCliFlag, H as resolveSkipPrefix, I as runDockerStreaming, J as MIGRATE_TMP_PREFIX, K as CFN_TEMPLATE_BODY_LIMIT, L as Synthesizer, M as buildDockerImage, N as formatDockerLoginError, O as shouldRetainResource, P as getDockerCmd, R as getDefaultStateBucketName, S as applyRoleArnIfSet, T as TemplateParser, U as resolveStateBucketWithDefault, V as resolveCaptureObservedState, W as resolveStateBucketWithDefaultAndSource, X as uploadCfnTemplate, Y as findLargeInlineResources, Z as AssemblyReader, _ as collectInlinePolicyNamesManagedBySiblings, a as withRetry, b as CloudControlProvider, c as extractDeploymentEventError, d as cyan, f as gray, g as IAMRoleProvider, h as yellow, i as withResourceDeadline, j as WorkGraph, k as AssetPublisher, l as formatResourceLine, m as red, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as green, q as CFN_TEMPLATE_URL_LIMIT, r as DeployEngine, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as bold, v as ProviderRegistry, w as DagBuilder, x as IntrinsicFunctionResolver, y as findActionableSilentDrops, z as getLegacyStateBucketName } from "./deploy-engine-CFaK71E9.js";
5
5
  import { t as ASGProvider } from "./asg-provider-B_hrCxRx.js";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import { randomBytes, randomUUID } from "node:crypto";
@@ -1507,6 +1507,387 @@ async function promptRecreateConfirm(input) {
1507
1507
  }
1508
1508
  }
1509
1509
 
1510
+ //#endregion
1511
+ //#region src/state/deployment-events-store.ts
1512
+ /**
1513
+ * Deployment-events store (issue #808) — persists the structured
1514
+ * deployment events emitted by the deploy engine / destroy runner as
1515
+ * JSONL to the state bucket, plus a small per-stack run index:
1516
+ *
1517
+ * - Events: `{prefix}/{stackName}/{region}/deployments/{runId}.jsonl`
1518
+ * - Index: `{prefix}/{stackName}/{region}/deployments/index.json`
1519
+ *
1520
+ * Design constraints (all load-bearing):
1521
+ *
1522
+ * - **Best-effort, never blocking**: `record()` is synchronous and only
1523
+ * buffers in memory; flushes run asynchronously (debounced timer +
1524
+ * size threshold) and are serialized on a write chain. A failed S3
1525
+ * write warns once and degrades to debug-level afterwards — it can
1526
+ * NEVER fail or block the deploy/destroy itself.
1527
+ * - **No locking**: each run writes to its own unique `{runId}.jsonl`
1528
+ * key (no concurrent writer by construction). `index.json` is
1529
+ * last-writer-wins — acceptable for a derived view; the `.jsonl`
1530
+ * files are the source of truth and `cdkd events` can read a run
1531
+ * directly by id even if the index lost the race.
1532
+ * - **No resource properties** in events — errors + metadata only
1533
+ * (properties may contain secrets and already live in state.json).
1534
+ * - **Separate keys from state.json** — no state schema bump; event
1535
+ * files survive `cdkd destroy` (state deletion does not touch
1536
+ * `deployments/`), preserving post-mortem context.
1537
+ */
1538
+ /** Max runs retained in `deployments/index.json` (newest first). */
1539
+ const DEPLOYMENT_EVENTS_MAX_INDEX_RUNS = 20;
1540
+ /** Debounce window between buffered events and the async S3 flush. */
1541
+ const FLUSH_INTERVAL_MS = 2e3;
1542
+ /** Flush immediately once this many events are buffered. */
1543
+ const FLUSH_EVENT_THRESHOLD = 50;
1544
+ /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1545
+ function getCdkdVersion() {
1546
+ return "0.220.0";
1547
+ }
1548
+ /**
1549
+ * Generate a time-sortable unique run id, e.g.
1550
+ * `20260613T012345678Z-1a2b3c4d`. The timestamp prefix keeps S3 listings
1551
+ * and `cdkd events` output chronologically meaningful; the random suffix
1552
+ * guarantees uniqueness across concurrent runs.
1553
+ */
1554
+ function newDeploymentRunId(now = /* @__PURE__ */ new Date()) {
1555
+ return `${now.toISOString().replace(/[-:.]/g, "")}-${randomUUID().slice(0, 8)}`;
1556
+ }
1557
+ /** S3 key of a run's JSONL event stream. */
1558
+ function deploymentEventsKey(prefix, stackName, region, runId) {
1559
+ return `${prefix}/${stackName}/${region}/deployments/${runId}.jsonl`;
1560
+ }
1561
+ /** S3 key of a stack's run index. */
1562
+ function deploymentEventsIndexKey(prefix, stackName, region) {
1563
+ return `${prefix}/${stackName}/${region}/deployments/index.json`;
1564
+ }
1565
+ /**
1566
+ * Buffering JSONL writer for one deployment run. Implements the
1567
+ * {@link DeploymentEventRecorder} seam the deploy engine / destroy
1568
+ * runner emit through.
1569
+ */
1570
+ var DeploymentEventsStore = class {
1571
+ logger = getLogger().child("DeploymentEvents");
1572
+ backend;
1573
+ stackName;
1574
+ region;
1575
+ command;
1576
+ runId;
1577
+ cdkdVersion;
1578
+ startedAt;
1579
+ /** All events recorded so far (the full JSONL body is re-PUT per flush —
1580
+ * S3 has no append, and metadata-only events are small). */
1581
+ events = [];
1582
+ /** Number of events already persisted by the last successful flush. */
1583
+ persistedCount = 0;
1584
+ flushTimer;
1585
+ /** Serializes S3 writes so flushes never interleave. */
1586
+ writeChain = Promise.resolve();
1587
+ warnedOnce = false;
1588
+ finalized = false;
1589
+ constructor(backend, options) {
1590
+ this.backend = backend;
1591
+ this.stackName = options.stackName;
1592
+ this.region = options.region;
1593
+ this.command = options.command;
1594
+ this.runId = options.runId ?? newDeploymentRunId();
1595
+ this.cdkdVersion = options.cdkdVersion ?? getCdkdVersion();
1596
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1597
+ }
1598
+ /**
1599
+ * Buffer one event (synchronous, never throws). The timestamp is
1600
+ * stamped here so emitters don't need to.
1601
+ */
1602
+ record(event) {
1603
+ try {
1604
+ if (this.finalized) return;
1605
+ this.events.push({
1606
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1607
+ ...event
1608
+ });
1609
+ if (this.events.length - this.persistedCount >= FLUSH_EVENT_THRESHOLD) this.scheduleFlush(0);
1610
+ else this.scheduleFlush(FLUSH_INTERVAL_MS);
1611
+ } catch {}
1612
+ }
1613
+ /**
1614
+ * Final flush + index update. Called by the owner (deploy CLI per-stack
1615
+ * finally / destroy runner finally) after the run reaches a terminal
1616
+ * state. Best-effort: never throws.
1617
+ */
1618
+ async finalize(result) {
1619
+ if (this.finalized) return;
1620
+ this.finalized = true;
1621
+ if (this.flushTimer) {
1622
+ clearTimeout(this.flushTimer);
1623
+ this.flushTimer = void 0;
1624
+ }
1625
+ if (this.events.length === 0) return;
1626
+ await this.enqueueWrite(async () => {
1627
+ await this.doFlush();
1628
+ await this.updateIndex(result);
1629
+ });
1630
+ }
1631
+ /** Await any in-flight async flushes (used by tests). */
1632
+ async drain() {
1633
+ await this.writeChain;
1634
+ }
1635
+ scheduleFlush(delayMs) {
1636
+ if (this.flushTimer) {
1637
+ if (delayMs > 0) return;
1638
+ clearTimeout(this.flushTimer);
1639
+ }
1640
+ this.flushTimer = setTimeout(() => {
1641
+ this.flushTimer = void 0;
1642
+ this.enqueueWrite(() => this.doFlush());
1643
+ }, delayMs);
1644
+ this.flushTimer.unref?.();
1645
+ }
1646
+ enqueueWrite(op) {
1647
+ const next = this.writeChain.then(op).catch((err) => {
1648
+ this.warnOnce(`Failed to persist deployment events for run ${this.runId}: ${err instanceof Error ? err.message : String(err)}`);
1649
+ });
1650
+ this.writeChain = next;
1651
+ return next;
1652
+ }
1653
+ async doFlush() {
1654
+ if (this.events.length === 0 || this.events.length === this.persistedCount) return;
1655
+ const snapshotCount = this.events.length;
1656
+ const body = this.events.slice(0, snapshotCount).map((e) => JSON.stringify(e)).join("\n");
1657
+ await this.backend.putRawObject(deploymentEventsKey(this.backend.prefix, this.stackName, this.region, this.runId), body + "\n", "application/x-ndjson");
1658
+ this.persistedCount = snapshotCount;
1659
+ }
1660
+ /**
1661
+ * Prepend this run's summary to `deployments/index.json`, truncated to
1662
+ * the last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs. Read-modify-
1663
+ * write WITHOUT optimistic locking — last-writer-wins (documented
1664
+ * trade-off; the per-run `.jsonl` files are the source of truth).
1665
+ */
1666
+ async updateIndex(result) {
1667
+ const key = deploymentEventsIndexKey(this.backend.prefix, this.stackName, this.region);
1668
+ let existingRuns = [];
1669
+ try {
1670
+ const raw = await this.backend.getRawObject(key);
1671
+ if (raw !== null) {
1672
+ const parsed = JSON.parse(raw);
1673
+ if (Array.isArray(parsed.runs)) existingRuns = parsed.runs;
1674
+ }
1675
+ } catch (err) {
1676
+ this.logger.debug(`Deployment-events index unreadable, rewriting: ${err instanceof Error ? err.message : String(err)}`);
1677
+ }
1678
+ const runs = [{
1679
+ runId: this.runId,
1680
+ command: this.command,
1681
+ cdkdVersion: this.cdkdVersion,
1682
+ startedAt: this.startedAt,
1683
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1684
+ result,
1685
+ eventCount: this.persistedCount
1686
+ }, ...existingRuns.filter((r) => r.runId !== this.runId)].slice(0, 20);
1687
+ const file = {
1688
+ indexVersion: 1,
1689
+ stackName: this.stackName,
1690
+ region: this.region,
1691
+ runs,
1692
+ lastModified: Date.now()
1693
+ };
1694
+ await this.backend.putRawObject(key, JSON.stringify(file, null, 2));
1695
+ }
1696
+ warnOnce(message) {
1697
+ if (this.warnedOnce) {
1698
+ this.logger.debug(message);
1699
+ return;
1700
+ }
1701
+ this.warnedOnce = true;
1702
+ this.logger.warn(`${message} — continuing; deployment events are best-effort and never block the run.`);
1703
+ }
1704
+ };
1705
+ /**
1706
+ * Read side for `cdkd events`: discovers regions / runs / event streams
1707
+ * under `{prefix}/{stackName}/{region}/deployments/`. Region discovery
1708
+ * deliberately does NOT rely on state.json — event files survive
1709
+ * `cdkd destroy`, so a destroyed stack's runs stay readable.
1710
+ */
1711
+ var DeploymentEventsReader = class {
1712
+ backend;
1713
+ constructor(backend) {
1714
+ this.backend = backend;
1715
+ }
1716
+ /**
1717
+ * Regions that have a `deployments/` index or event stream for the
1718
+ * stack. Derived from the raw key listing under `{prefix}/{stackName}/`.
1719
+ */
1720
+ async listRegions(stackName) {
1721
+ const prefix = `${this.backend.prefix}/${stackName}/`;
1722
+ const keys = await this.backend.listRawKeys(prefix);
1723
+ const regions = /* @__PURE__ */ new Set();
1724
+ for (const key of keys) {
1725
+ const segments = key.slice(prefix.length).split("/");
1726
+ if (segments.length === 3 && segments[1] === "deployments" && segments[0]) regions.add(segments[0]);
1727
+ }
1728
+ return [...regions].sort();
1729
+ }
1730
+ /**
1731
+ * Run summaries for `(stackName, region)`, newest first. Returns the
1732
+ * index file's `runs` (already newest-first); when the index is missing
1733
+ * or unreadable, falls back to enumerating `{runId}.jsonl` keys (sorted
1734
+ * descending — runIds are time-prefixed).
1735
+ *
1736
+ * In the fallback path the run's terminal result / command / version are
1737
+ * NOT known from the key alone, so each run's JSONL is read and its last
1738
+ * `RUN_FINISHED` (+ first `RUN_STARTED`) event mined for the true result,
1739
+ * command, version, and timestamps. A run whose JSONL has no terminal
1740
+ * `RUN_FINISHED` (interrupted run, or one whose index write lost the
1741
+ * race) is reported as `'UNKNOWN'` — it is NEVER fabricated as `'FAILED'`,
1742
+ * which would mislabel a successful run that merely failed to update the
1743
+ * derived index.
1744
+ */
1745
+ async listRuns(stackName, region) {
1746
+ const key = deploymentEventsIndexKey(this.backend.prefix, stackName, region);
1747
+ try {
1748
+ const raw = await this.backend.getRawObject(key);
1749
+ if (raw !== null) {
1750
+ const parsed = JSON.parse(raw);
1751
+ if (Array.isArray(parsed.runs)) return parsed.runs;
1752
+ }
1753
+ } catch {}
1754
+ const dirPrefix = `${this.backend.prefix}/${stackName}/${region}/deployments/`;
1755
+ const runIds = (await this.backend.listRawKeys(dirPrefix)).filter((k) => k.endsWith(".jsonl")).map((k) => k.slice(dirPrefix.length, -6)).sort().reverse();
1756
+ return Promise.all(runIds.map((runId) => this.summarizeRunFromJsonl(stackName, region, runId)));
1757
+ }
1758
+ /**
1759
+ * Reconstruct a {@link DeploymentRunSummary} for the index-fallback path
1760
+ * by reading the run's JSONL. Derives the true result from the last
1761
+ * `RUN_FINISHED` event; absent that, returns `'UNKNOWN'` rather than
1762
+ * fabricating a definitive failure.
1763
+ */
1764
+ async summarizeRunFromJsonl(stackName, region, runId) {
1765
+ const fallback = {
1766
+ runId,
1767
+ command: "deploy",
1768
+ cdkdVersion: "unknown",
1769
+ startedAt: "",
1770
+ finishedAt: "",
1771
+ result: "UNKNOWN",
1772
+ eventCount: 0
1773
+ };
1774
+ let events = null;
1775
+ try {
1776
+ events = await this.readRunEvents(stackName, region, runId);
1777
+ } catch {
1778
+ return fallback;
1779
+ }
1780
+ if (events === null || events.length === 0) return fallback;
1781
+ const started = events.find((e) => e.eventType === "RUN_STARTED");
1782
+ let finished;
1783
+ for (const e of events) if (e.eventType === "RUN_FINISHED") finished = e;
1784
+ return {
1785
+ runId,
1786
+ command: started?.command ?? fallback.command,
1787
+ cdkdVersion: started?.cdkdVersion ?? fallback.cdkdVersion,
1788
+ startedAt: started?.timestamp ?? fallback.startedAt,
1789
+ finishedAt: finished?.timestamp ?? fallback.finishedAt,
1790
+ result: finished?.result ?? "UNKNOWN",
1791
+ eventCount: events.length
1792
+ };
1793
+ }
1794
+ /**
1795
+ * Parse one run's JSONL event stream. Returns `null` when the run file
1796
+ * does not exist. Malformed lines are skipped (a torn final line from
1797
+ * an interrupted flush must not hide the rest of the stream).
1798
+ */
1799
+ async readRunEvents(stackName, region, runId) {
1800
+ const key = deploymentEventsKey(this.backend.prefix, stackName, region, runId);
1801
+ const raw = await this.backend.getRawObject(key);
1802
+ if (raw === null) return null;
1803
+ const events = [];
1804
+ for (const line of raw.split("\n")) {
1805
+ const trimmed = line.trim();
1806
+ if (!trimmed) continue;
1807
+ try {
1808
+ events.push(JSON.parse(trimmed));
1809
+ } catch {}
1810
+ }
1811
+ return events;
1812
+ }
1813
+ };
1814
+
1815
+ //#endregion
1816
+ //#region src/cli/commands/deployment-events-run.ts
1817
+ /**
1818
+ * Run-level deployment-event bracket helpers (issue #808).
1819
+ *
1820
+ * The per-resource + rollback events are emitted by `DeployEngine` /
1821
+ * `destroy-runner.ts`; the RUN-level events (`RUN_STARTED` /
1822
+ * `RUN_FINISHED`) and the recorder lifecycle (`finalize()` in a `finally`)
1823
+ * are owned by the `cdkd deploy` / `cdkd destroy` CLIs. That run-level
1824
+ * bracket was originally inline in `deploy.ts` / `destroy.ts` and could
1825
+ * only be exercised through the full synth → STS → work-graph pipeline.
1826
+ *
1827
+ * These thin helpers extract the bracket so it is directly unit-testable
1828
+ * (and shared between the two commands), with the exact contract:
1829
+ *
1830
+ * - **`--dry-run` creates NO recorder** (no events at all for a dry run).
1831
+ * - `RUN_STARTED` is emitted the moment the recorder is created.
1832
+ * - A successful run emits `RUN_FINISHED { result: 'SUCCEEDED', counts }`.
1833
+ * - A failed run emits `RUN_FINISHED { result: 'FAILED', error }` via
1834
+ * {@link extractDeploymentEventError} (error metadata only — no props).
1835
+ * - `finalize(result)` is always called (caller's `finally`).
1836
+ *
1837
+ * All of this is best-effort: the recorder's `record()` / `finalize()`
1838
+ * never throw, so these helpers never need their own try/catch.
1839
+ */
1840
+ /**
1841
+ * Create a {@link DeploymentEventsStore} and immediately emit its
1842
+ * `RUN_STARTED` event. Returns `undefined` under `--dry-run` (no recorder,
1843
+ * no events). The caller wires the returned recorder into the engine /
1844
+ * runner and `finalize()`s it in a `finally`.
1845
+ */
1846
+ function startRunRecorder(args) {
1847
+ if (args.dryRun) return void 0;
1848
+ const recorder = new DeploymentEventsStore(args.backend, {
1849
+ stackName: args.stackName,
1850
+ region: args.region,
1851
+ command: args.command,
1852
+ ...args.runId !== void 0 && { runId: args.runId },
1853
+ ...args.cdkdVersion !== void 0 && { cdkdVersion: args.cdkdVersion }
1854
+ });
1855
+ recorder.record({
1856
+ eventType: "RUN_STARTED",
1857
+ stackName: args.stackName,
1858
+ command: args.command,
1859
+ region: args.region,
1860
+ cdkdVersion: recorder.cdkdVersion
1861
+ });
1862
+ return recorder;
1863
+ }
1864
+ /**
1865
+ * Emit a success `RUN_FINISHED`. No-op when `recorder` is `undefined`
1866
+ * (dry-run / older state). `durationMs` is optional (destroy does not
1867
+ * carry one at the run level).
1868
+ */
1869
+ function recordRunSucceeded(recorder, stackName, counts, durationMs) {
1870
+ recorder?.record({
1871
+ eventType: "RUN_FINISHED",
1872
+ stackName,
1873
+ result: "SUCCEEDED",
1874
+ ...durationMs !== void 0 && { durationMs },
1875
+ counts
1876
+ });
1877
+ }
1878
+ /**
1879
+ * Emit a failure `RUN_FINISHED` carrying the extracted error metadata.
1880
+ * No-op when `recorder` is `undefined`.
1881
+ */
1882
+ function recordRunFailed(recorder, stackName, error) {
1883
+ recorder?.record({
1884
+ eventType: "RUN_FINISHED",
1885
+ stackName,
1886
+ result: "FAILED",
1887
+ error: extractDeploymentEventError(error)
1888
+ });
1889
+ }
1890
+
1510
1891
  //#endregion
1511
1892
  //#region src/state/export-index-store.ts
1512
1893
  /**
@@ -33762,8 +34143,20 @@ function countProtectedResources(state) {
33762
34143
  * - Switches `process.env.AWS_REGION` for the duration of the destroy when
33763
34144
  * the stack's recorded region differs from `baseRegion`. Restored in the
33764
34145
  * `finally` block.
34146
+ * - Persists state incrementally during the delete loop (issue #804):
34147
+ * each successfully deleted resource is removed from the state object
34148
+ * and the trimmed state is written back to S3, mirroring deploy's
34149
+ * `saveStateAfterResource`. An interrupted destroy therefore leaves a
34150
+ * state file listing only resources that still exist, so a re-run does
34151
+ * not replay deletes against already-deleted resources. Persist
34152
+ * failures are logged and never fail the destroy. Every persisted
34153
+ * destroy snapshot CLEARS `outputs` (and drops `imports` / `outputReads`)
34154
+ * so a partial-destroy state never advertises an export / import whose
34155
+ * backing resource is gone — the in-memory `state` the strong-ref check
34156
+ * above reads is untouched.
33765
34157
  * - On full success, deletes the state file. On any failure, the state
33766
- * file is preserved so the user can retry.
34158
+ * file is preserved (trimmed to the remaining resources, outputs/imports
34159
+ * cleared) so the user can retry.
33767
34160
  */
33768
34161
  async function runDestroyForStack(stackName, state, ctx) {
33769
34162
  const logger = getLogger();
@@ -33843,6 +34236,27 @@ async function runDestroyForStack(stackName, state, ctx) {
33843
34236
  throw new StackHasActiveImportsError(stackName, regionForState, consumers);
33844
34237
  }
33845
34238
  }
34239
+ const remainingResources = { ...state.resources };
34240
+ const buildDestroySnapshot = () => {
34241
+ const { imports: _imports, outputReads: _outputReads, ...rest } = state;
34242
+ return {
34243
+ ...rest,
34244
+ resources: { ...remainingResources },
34245
+ outputs: {},
34246
+ lastModified: Date.now()
34247
+ };
34248
+ };
34249
+ let saveChain = Promise.resolve();
34250
+ const persistStateAfterDelete = (logicalId) => {
34251
+ saveChain = saveChain.then(async () => {
34252
+ try {
34253
+ await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
34254
+ logger.debug(`State persisted after deleting ${logicalId}`);
34255
+ } catch (error) {
34256
+ logger.warn(`Failed to persist state after deleting ${logicalId} (continuing): ${error instanceof Error ? error.message : String(error)}`);
34257
+ }
34258
+ });
34259
+ };
33846
34260
  const renderer = getLiveRenderer();
33847
34261
  renderer.start();
33848
34262
  try {
@@ -33899,10 +34313,27 @@ async function runDestroyForStack(stackName, state, ctx) {
33899
34313
  if (shouldRetainResource(resource.deletionPolicy)) {
33900
34314
  logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
33901
34315
  result.retainedCount++;
34316
+ ctx.eventRecorder?.record({
34317
+ eventType: "RESOURCE_RETAINED",
34318
+ stackName,
34319
+ operation: "DELETE",
34320
+ logicalId,
34321
+ resourceType: resource.resourceType,
34322
+ ...resource.provisionedBy && { provisionedBy: resource.provisionedBy }
34323
+ });
33902
34324
  return;
33903
34325
  }
33904
34326
  const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
33905
34327
  renderer.addTask(logicalId, baseLabel);
34328
+ const resourceStartedAt = Date.now();
34329
+ ctx.eventRecorder?.record({
34330
+ eventType: "RESOURCE_STARTED",
34331
+ stackName,
34332
+ operation: "DELETE",
34333
+ logicalId,
34334
+ resourceType: resource.resourceType,
34335
+ ...resource.provisionedBy && { provisionedBy: resource.provisionedBy }
34336
+ });
33906
34337
  try {
33907
34338
  const provider = destroyProviderRegistry.getProviderFor({
33908
34339
  resourceType: resource.resourceType,
@@ -33946,19 +34377,63 @@ async function runDestroyForStack(stackName, state, ctx) {
33946
34377
  renderer.removeTask(logicalId);
33947
34378
  logger.info(` ${formatResourceLine("deleted", logicalId, resource.resourceType)}`);
33948
34379
  result.deletedCount++;
34380
+ ctx.eventRecorder?.record({
34381
+ eventType: "RESOURCE_SUCCEEDED",
34382
+ stackName,
34383
+ operation: "DELETE",
34384
+ logicalId,
34385
+ resourceType: resource.resourceType,
34386
+ ...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
34387
+ ...resource.physicalId && { physicalId: resource.physicalId },
34388
+ durationMs: Date.now() - resourceStartedAt
34389
+ });
34390
+ delete remainingResources[logicalId];
34391
+ persistStateAfterDelete(logicalId);
33949
34392
  } catch (error) {
33950
34393
  renderer.removeTask(logicalId);
33951
34394
  const msg = error instanceof Error ? error.message : String(error);
33952
34395
  if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
33953
34396
  logger.debug(` ${logicalId} already deleted, removing from state`);
33954
34397
  result.deletedCount++;
34398
+ ctx.eventRecorder?.record({
34399
+ eventType: "RESOURCE_SUCCEEDED",
34400
+ stackName,
34401
+ operation: "DELETE",
34402
+ logicalId,
34403
+ resourceType: resource.resourceType,
34404
+ ...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
34405
+ ...resource.physicalId && { physicalId: resource.physicalId },
34406
+ durationMs: Date.now() - resourceStartedAt
34407
+ });
34408
+ delete remainingResources[logicalId];
34409
+ persistStateAfterDelete(logicalId);
33955
34410
  } else if (error instanceof ResourceTimeoutError) {
33956
34411
  const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
33957
34412
  logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
33958
34413
  result.errorCount++;
34414
+ ctx.eventRecorder?.record({
34415
+ eventType: "RESOURCE_FAILED",
34416
+ stackName,
34417
+ operation: "DELETE",
34418
+ logicalId,
34419
+ resourceType: resource.resourceType,
34420
+ ...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
34421
+ durationMs: Date.now() - resourceStartedAt,
34422
+ error: extractDeploymentEventError(wrapped)
34423
+ });
33959
34424
  } else {
33960
34425
  logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
33961
34426
  result.errorCount++;
34427
+ ctx.eventRecorder?.record({
34428
+ eventType: "RESOURCE_FAILED",
34429
+ stackName,
34430
+ operation: "DELETE",
34431
+ logicalId,
34432
+ resourceType: resource.resourceType,
34433
+ ...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
34434
+ durationMs: Date.now() - resourceStartedAt,
34435
+ error: extractDeploymentEventError(error)
34436
+ });
33962
34437
  }
33963
34438
  } finally {
33964
34439
  renderer.removeTask(logicalId);
@@ -33966,16 +34441,25 @@ async function runDestroyForStack(stackName, state, ctx) {
33966
34441
  });
33967
34442
  await Promise.all(deletePromises);
33968
34443
  }
34444
+ await saveChain;
33969
34445
  if (result.errorCount === 0) {
33970
34446
  await ctx.stateBackend.deleteState(stackName, regionForState);
33971
34447
  logger.debug("State deleted");
33972
34448
  if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
33973
- } else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
34449
+ } else {
34450
+ try {
34451
+ await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
34452
+ } catch (error) {
34453
+ logger.warn(`Failed to persist remaining state after partial destroy: ${error instanceof Error ? error.message : String(error)}. The state file may still list already-deleted resources; a re-run resolves them idempotently.`);
34454
+ }
34455
+ logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
34456
+ }
33974
34457
  const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
33975
34458
  if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
33976
34459
  else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
33977
34460
  } finally {
33978
34461
  renderer.stop();
34462
+ await saveChain;
33979
34463
  logger.debug("Releasing lock...");
33980
34464
  await ctx.lockManager.releaseLock(stackName, regionForState);
33981
34465
  if (destroyAwsClients) {
@@ -35051,6 +35535,14 @@ async function deployCommand(stacks, options) {
35051
35535
  stackProviderRegistry.setCustomResourceResponseBucket(stateBucket, baseRegion);
35052
35536
  if (options.allowUnsupportedTypes?.length) stackProviderRegistry.allowUnsupportedTypes(options.allowUnsupportedTypes);
35053
35537
  if (options.allowUnsupportedProperties?.length) stackProviderRegistry.allowUnsupportedProperties(options.allowUnsupportedProperties);
35538
+ const eventRecorder = startRunRecorder({
35539
+ backend: stackStateBackend,
35540
+ stackName: stackInfo.stackName,
35541
+ region: stackRegion,
35542
+ command: "deploy",
35543
+ dryRun: options.dryRun
35544
+ });
35545
+ let runResult = "SUCCEEDED";
35054
35546
  try {
35055
35547
  if (skipPrefix) {
35056
35548
  const existing = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
@@ -35106,6 +35598,7 @@ async function deployCommand(stacks, options) {
35106
35598
  concurrency: options.concurrency,
35107
35599
  dryRun: options.dryRun,
35108
35600
  noRollback: !options.rollback,
35601
+ ...eventRecorder && { eventRecorder },
35109
35602
  ...recreateViaCcApiTargets && recreateViaCcApiTargets.size > 0 && { recreateViaCcApiTargets },
35110
35603
  ...recreateViaSdkProviderTargets && recreateViaSdkProviderTargets.size > 0 && { recreateViaSdkProviderTargets },
35111
35604
  captureObservedState: resolveCaptureObservedState(options.captureObservedState),
@@ -35143,7 +35636,17 @@ async function deployCommand(stacks, options) {
35143
35636
  }
35144
35637
  if (options.dryRun) logger.info(`\n${green("✓")} ${bold("Dry run completed")} - no actual changes made`);
35145
35638
  else logger.info(`\n${green("✓")} ${bold("Deployment completed successfully")}`);
35639
+ recordRunSucceeded(eventRecorder, stackInfo.stackName, {
35640
+ created: deployResult.created,
35641
+ updated: deployResult.updated,
35642
+ deleted: deployResult.deleted
35643
+ }, deployResult.durationMs);
35644
+ } catch (deployError) {
35645
+ runResult = "FAILED";
35646
+ recordRunFailed(eventRecorder, stackInfo.stackName, deployError);
35647
+ throw deployError;
35146
35648
  } finally {
35649
+ if (eventRecorder) await eventRecorder.finalize(runResult);
35147
35650
  stackAwsClients.destroy();
35148
35651
  stateS3Client.destroy();
35149
35652
  switchRegion(baseRegion);
@@ -37645,42 +38148,70 @@ async function destroyCommand(stackArgs, options) {
37645
38148
  totalErrors++;
37646
38149
  continue;
37647
38150
  }
37648
- const result = await withNestedStackContext({
37649
- stateBackend,
37650
- lockManager,
37651
- providerRegistry,
37652
- parentStackName: stackName,
37653
- parentRegion: stackTargetRegion,
37654
- accountId,
37655
- awsClients,
37656
- stateBucket,
37657
- exportIndexStore,
37658
- destroyOptions: {
38151
+ const eventRecorder = startRunRecorder({
38152
+ backend: stateBackend,
38153
+ stackName,
38154
+ region: stackTargetRegion,
38155
+ command: "destroy"
38156
+ });
38157
+ let destroyRunResult = "SUCCEEDED";
38158
+ try {
38159
+ const result = await withNestedStackContext({
38160
+ stateBackend,
38161
+ lockManager,
38162
+ providerRegistry,
38163
+ parentStackName: stackName,
38164
+ parentRegion: stackTargetRegion,
38165
+ accountId,
38166
+ awsClients,
38167
+ stateBucket,
38168
+ exportIndexStore,
38169
+ destroyOptions: {
38170
+ ...options.profile && { profile: options.profile },
38171
+ ...options.removeProtection === true && { removeProtection: true },
38172
+ ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
38173
+ ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
38174
+ ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
38175
+ ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
38176
+ }
38177
+ }, () => runDestroyForStack(stackName, stateResult.state, {
38178
+ stateBackend,
38179
+ lockManager,
38180
+ providerRegistry,
38181
+ baseAwsClients: awsClients,
38182
+ baseRegion: region,
37659
38183
  ...options.profile && { profile: options.profile },
37660
- ...options.removeProtection === true && { removeProtection: true },
38184
+ stateBucket,
38185
+ skipConfirmation: options.yes || options.force,
38186
+ removeProtection: options.removeProtection === true,
38187
+ exportIndexStore,
38188
+ ...options.allowUnsupportedTypes?.length && { allowUnsupportedTypes: options.allowUnsupportedTypes },
37661
38189
  ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
37662
38190
  ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
37663
38191
  ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
37664
- ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
37665
- }
37666
- }, () => runDestroyForStack(stackName, stateResult.state, {
37667
- stateBackend,
37668
- lockManager,
37669
- providerRegistry,
37670
- baseAwsClients: awsClients,
37671
- baseRegion: region,
37672
- ...options.profile && { profile: options.profile },
37673
- stateBucket,
37674
- skipConfirmation: options.yes || options.force,
37675
- removeProtection: options.removeProtection === true,
37676
- exportIndexStore,
37677
- ...options.allowUnsupportedTypes?.length && { allowUnsupportedTypes: options.allowUnsupportedTypes },
37678
- ...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
37679
- ...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
37680
- ...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
37681
- ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
37682
- }));
37683
- totalErrors += result.errorCount;
38192
+ ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs },
38193
+ eventRecorder
38194
+ }));
38195
+ totalErrors += result.errorCount;
38196
+ destroyRunResult = result.errorCount > 0 ? "FAILED" : "SUCCEEDED";
38197
+ eventRecorder.record({
38198
+ eventType: "RUN_FINISHED",
38199
+ stackName,
38200
+ result: destroyRunResult,
38201
+ counts: {
38202
+ created: 0,
38203
+ updated: 0,
38204
+ deleted: result.deletedCount,
38205
+ ...result.errorCount > 0 && { failed: result.errorCount }
38206
+ }
38207
+ });
38208
+ } catch (destroyError) {
38209
+ destroyRunResult = "FAILED";
38210
+ recordRunFailed(eventRecorder, stackName, destroyError);
38211
+ throw destroyError;
38212
+ } finally {
38213
+ await eventRecorder.finalize(destroyRunResult);
38214
+ }
37684
38215
  }
37685
38216
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry.`);
37686
38217
  } finally {
@@ -37705,6 +38236,147 @@ function createDestroyCommand() {
37705
38236
  return cmd;
37706
38237
  }
37707
38238
 
38239
+ //#endregion
38240
+ //#region src/cli/commands/events.ts
38241
+ /**
38242
+ * `cdkd events <stack>` — read back the structured deployment events
38243
+ * (issue #808) cdkd persists per deploy / destroy run, the local
38244
+ * equivalent of CloudFormation's `DescribeStackEvents`.
38245
+ *
38246
+ * Two modes:
38247
+ * - No `--run`: list the recorded runs for the stack, newest first
38248
+ * (runId, command, cdkd version, result, started/finished, event count).
38249
+ * - `--run <id>`: print the full ordered event stream for that one run.
38250
+ *
38251
+ * `--format json` (alias `--json`) emits machine-readable JSON for tooling
38252
+ * / AI-agent hand-off. Events survive `cdkd destroy` (they live under a
38253
+ * separate `deployments/` key family from `state.json`), so a destroyed
38254
+ * stack's failure history stays readable.
38255
+ */
38256
+ async function eventsCommand(stackName, options) {
38257
+ const logger = getLogger();
38258
+ if (options.verbose) logger.setLevel("debug");
38259
+ warnIfDeprecatedRegion(options);
38260
+ const asJson = options.json === true;
38261
+ const awsClients = new AwsClients({
38262
+ ...options.region && { region: options.region },
38263
+ ...options.profile && { profile: options.profile }
38264
+ });
38265
+ setAwsClients(awsClients);
38266
+ try {
38267
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
38268
+ const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
38269
+ const prefix = options.statePrefix ?? "cdkd";
38270
+ const stateBackend = new S3StateBackend(awsClients.s3, {
38271
+ bucket,
38272
+ prefix
38273
+ }, {
38274
+ region,
38275
+ ...options.profile && { profile: options.profile }
38276
+ });
38277
+ await stateBackend.verifyBucketExists();
38278
+ const reader = new DeploymentEventsReader(stateBackend);
38279
+ const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
38280
+ if (options.run) {
38281
+ const events = await reader.readRunEvents(stackName, targetRegion, options.run);
38282
+ if (events === null) throw new CdkdError(`No deployment-event stream found for run '${options.run}' of stack '${stackName}' in region '${targetRegion}'.`, "EVENTS_RUN_NOT_FOUND");
38283
+ if (asJson) {
38284
+ process.stdout.write(JSON.stringify(events, null, 2) + "\n");
38285
+ return;
38286
+ }
38287
+ printRunEvents(stackName, targetRegion, options.run, events);
38288
+ return;
38289
+ }
38290
+ const runs = await reader.listRuns(stackName, targetRegion);
38291
+ if (asJson) {
38292
+ process.stdout.write(JSON.stringify({
38293
+ stackName,
38294
+ region: targetRegion,
38295
+ runs
38296
+ }, null, 2) + "\n");
38297
+ return;
38298
+ }
38299
+ printRunList(stackName, targetRegion, runs);
38300
+ } finally {
38301
+ awsClients.destroy();
38302
+ }
38303
+ }
38304
+ /**
38305
+ * Pick the region whose `deployments/` key family holds this stack's run
38306
+ * history. When `--stack-region` is supplied it is honored verbatim;
38307
+ * otherwise the single discovered region is used, and an ambiguous (>1)
38308
+ * or missing (0) history surfaces an actionable error.
38309
+ */
38310
+ async function resolveEventsRegion(reader, stackName, explicitRegion) {
38311
+ if (explicitRegion) return explicitRegion;
38312
+ const regions = await reader.listRegions(stackName);
38313
+ if (regions.length === 0) throw new CdkdError(`No deployment-event history found for stack '${stackName}'. Events are recorded by 'cdkd deploy' / 'cdkd destroy' (issue #808); a stack deployed by an older cdkd version has none.`, "EVENTS_NOT_FOUND");
38314
+ if (regions.length > 1) throw new CdkdError(`Stack '${stackName}' has deployment-event history in multiple regions: ${regions.join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
38315
+ return regions[0];
38316
+ }
38317
+ /** Human-readable run listing (newest first). */
38318
+ function printRunList(stackName, region, runs) {
38319
+ const logger = getLogger();
38320
+ logger.info(`${bold("Deployment runs for")} ${cyan(stackName)} ${gray(`(${region})`)}`);
38321
+ if (runs.length === 0) {
38322
+ logger.info(gray(" (no runs recorded)"));
38323
+ return;
38324
+ }
38325
+ for (const run of runs) {
38326
+ const resultColored = run.result === "SUCCEEDED" ? green(run.result) : run.result === "UNKNOWN" ? gray(run.result) : red(run.result);
38327
+ logger.info(` ${cyan(run.runId)} ${run.command} ${resultColored} ${gray(run.startedAt || "?")} -> ${gray(run.finishedAt || "?")} ${gray(`cdkd ${run.cdkdVersion}`)} ${gray(`${run.eventCount} events`)}`);
38328
+ }
38329
+ logger.info(gray(`\nUse 'cdkd events ${stackName} --run <runId>' to read one run's events.`));
38330
+ }
38331
+ /** Human-readable single-run event stream (in recorded order). */
38332
+ function printRunEvents(stackName, region, runId, events) {
38333
+ const logger = getLogger();
38334
+ logger.info(`${bold("Events for run")} ${cyan(runId)} ${gray(`(${stackName}, ${region})`)}`);
38335
+ if (events.length === 0) {
38336
+ logger.info(gray(" (no events)"));
38337
+ return;
38338
+ }
38339
+ for (const e of events) {
38340
+ const parts = [gray(e.timestamp), colorizeEventType(e.eventType)];
38341
+ if (e.logicalId) parts.push(`${e.logicalId}${e.resourceType ? ` (${e.resourceType})` : ""}`);
38342
+ if (e.operation) parts.push(gray(e.operation));
38343
+ if (e.provisionedBy) parts.push(gray(`[${e.provisionedBy}]`));
38344
+ if (e.command) parts.push(gray(e.command));
38345
+ if (e.region) parts.push(gray(e.region));
38346
+ if (e.cdkdVersion) parts.push(gray(`cdkd ${e.cdkdVersion}`));
38347
+ if (e.result) parts.push(e.result === "SUCCEEDED" ? green(e.result) : red(e.result));
38348
+ if (typeof e.durationMs === "number") parts.push(gray(`${e.durationMs}ms`));
38349
+ if (e.counts) parts.push(gray(`+${e.counts.created}/~${e.counts.updated}/-${e.counts.deleted}` + (e.counts.failed ? ` !${e.counts.failed}` : "")));
38350
+ logger.info(` ${parts.join(" ")}`);
38351
+ if (e.error) {
38352
+ const code = e.error.awsErrorCode ? ` (${e.error.awsErrorCode})` : "";
38353
+ const reqId = e.error.requestId ? gray(` requestId=${e.error.requestId}`) : "";
38354
+ logger.info(` ${red(`${e.error.name}${code}: ${e.error.message}`)}${reqId}`);
38355
+ }
38356
+ }
38357
+ }
38358
+ /** Color the event-type token by its lifecycle phase. */
38359
+ function colorizeEventType(eventType) {
38360
+ if (eventType.endsWith("FAILED")) return red(eventType);
38361
+ if (eventType.endsWith("SUCCEEDED") || eventType === "RUN_FINISHED") return green(eventType);
38362
+ if (eventType.startsWith("ROLLBACK")) return yellow(eventType);
38363
+ return cyan(eventType);
38364
+ }
38365
+ /**
38366
+ * Create the `cdkd events` command.
38367
+ */
38368
+ function createEventsCommand() {
38369
+ const cmd = new Command("events").description("Read back structured deployment events (cdkd's DescribeStackEvents equivalent, issue #808)").argument("<stack>", "Stack name (physical CloudFormation name)").option("--run <runId>", "Read a single run's full event stream instead of the run listing").option("--stack-region <region>", "Disambiguate a stack with event history in multiple regions").option("--json", "Output as JSON", false).option("--format <format>", "Output format ('json' is equivalent to --json)").action(withErrorHandling((stack, options) => {
38370
+ return eventsCommand(stack, {
38371
+ ...options,
38372
+ json: options.json === true || options.format === "json"
38373
+ });
38374
+ }));
38375
+ [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
38376
+ cmd.addOption(deprecatedRegionOption);
38377
+ return cmd;
38378
+ }
38379
+
37708
38380
  //#endregion
37709
38381
  //#region src/cli/cdk-path.ts
37710
38382
  /**
@@ -52868,7 +53540,7 @@ function reorderArgs(argv) {
52868
53540
  async function main() {
52869
53541
  installPipeCloseHandler();
52870
53542
  const program = new Command();
52871
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.219.8");
53543
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.220.0");
52872
53544
  program.addCommand(createBootstrapCommand());
52873
53545
  program.addCommand(createSynthCommand());
52874
53546
  program.addCommand(createListCommand());
@@ -52876,6 +53548,7 @@ async function main() {
52876
53548
  program.addCommand(createDiffCommand());
52877
53549
  program.addCommand(createDriftCommand());
52878
53550
  program.addCommand(createDestroyCommand());
53551
+ program.addCommand(createEventsCommand());
52879
53552
  program.addCommand(createOrphanCommand());
52880
53553
  program.addCommand(createImportCommand());
52881
53554
  program.addCommand(createPublishAssetsCommand());