@go-to-k/cdkd 0.227.1 → 0.228.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
@@ -588,12 +588,15 @@ stream to S3 — cdkd's local equivalent of CloudFormation's
588
588
  cdkd events MyStack # list runs, newest first
589
589
  cdkd events MyStack --run <runId> # one run's full event stream
590
590
  cdkd events MyStack --format json # machine-readable (AI-agent hand-off)
591
+ cdkd events prune MyStack --all # purge event history (reclaim S3 space)
591
592
  ```
592
593
 
593
594
  Events are persisted as JSONL under a `deployments/` key family separate
594
595
  from `state.json` (no state schema bump), so a destroyed stack's failure
595
596
  history stays readable. Recording is best-effort and never blocks the
596
- run; events carry error + metadata only (never resource properties). See
597
+ run; events carry error + metadata only (never resource properties). The
598
+ store self-bounds to the last 20 runs, and `cdkd events prune` purges old
599
+ history on demand (`--keep N` / `--older-than <dur>` / `--all`). See
597
600
  **[docs/deployment-events.md](docs/deployment-events.md)** for the full
598
601
  reference.
599
602
 
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 { $ as findLargeInlineResources, A as shouldRetainResource, B as getDockerImageBySourceHash, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as formatDockerLoginError, G as resolveCaptureObservedState, H as getDefaultStateBucketName, I as getDockerCmd, J as resolveStateBucketWithDefaultAndSource, K as resolveSkipPrefix, L as runDockerForeground, M as stringifyValue, N as WorkGraph, O as S3StateBackend, P as buildDockerImage, Q as MIGRATE_TMP_PREFIX, R as runDockerStreaming, S as IntrinsicFunctionResolver, T as DagBuilder, U as getLegacyStateBucketName, V as Synthesizer, W as resolveApp, X as CFN_TEMPLATE_BODY_LIMIT, Y as warnDeprecatedNoPrefixCliFlag, Z as CFN_TEMPLATE_URL_LIMIT, _ as IAMRoleProvider, a as withRetry, b as findActionableSilentDrops, c as computeImplicitDeleteEdges, d as bold, et as uploadCfnTemplate, f as cyan, g as yellow, h as red, i as withResourceDeadline, j as AssetPublisher, k as rebuildClientForBucketRegion, l as extractDeploymentEventError, m as green, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as gray, q as resolveStateBucketWithDefault, r as DeployEngine, rt as resolveBucketRegion, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as AssemblyReader, u as formatResourceLine, v as collectInlinePolicyNamesManagedBySiblings, w as DiffCalculator, x as CloudControlProvider, y as ProviderRegistry, z as AssetManifestLoader } from "./deploy-engine-Bl_Ka0hj.js";
4
+ import { $ as findLargeInlineResources, A as shouldRetainResource, B as getDockerImageBySourceHash, C as applyRoleArnIfSet, D as LockManager, E as TemplateParser, F as formatDockerLoginError, G as resolveCaptureObservedState, H as getDefaultStateBucketName, I as getDockerCmd, J as resolveStateBucketWithDefaultAndSource, K as resolveSkipPrefix, L as runDockerForeground, M as stringifyValue, N as WorkGraph, O as S3StateBackend, P as buildDockerImage, Q as MIGRATE_TMP_PREFIX, R as runDockerStreaming, S as IntrinsicFunctionResolver, T as DagBuilder, U as getLegacyStateBucketName, V as Synthesizer, W as resolveApp, X as CFN_TEMPLATE_BODY_LIMIT, Y as warnDeprecatedNoPrefixCliFlag, Z as CFN_TEMPLATE_URL_LIMIT, _ as IAMRoleProvider, a as withRetry, b as findActionableSilentDrops, c as computeImplicitDeleteEdges, d as bold, et as uploadCfnTemplate, f as cyan, g as yellow, h as red, i as withResourceDeadline, j as AssetPublisher, k as rebuildClientForBucketRegion, l as extractDeploymentEventError, m as green, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as gray, q as resolveStateBucketWithDefault, r as DeployEngine, rt as resolveBucketRegion, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as AssemblyReader, u as formatResourceLine, v as collectInlinePolicyNamesManagedBySiblings, w as DiffCalculator, x as CloudControlProvider, y as ProviderRegistry, z as AssetManifestLoader } from "./deploy-engine-BRZdKXDs.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";
@@ -1543,6 +1543,10 @@ async function promptRecreateConfirm(input) {
1543
1543
  * - **Separate keys from state.json** — no state schema bump; event
1544
1544
  * files survive `cdkd destroy` (state deletion does not touch
1545
1545
  * `deployments/`), preserving post-mortem context.
1546
+ * - **Bounded growth** (issue #885): `finalize()` prunes `{runId}.jsonl`
1547
+ * streams that fell out of the index window so the `deployments/`
1548
+ * prefix stays bounded; `cdkd events prune` (via
1549
+ * {@link DeploymentEventsReader.pruneRuns}) is the explicit purge.
1546
1550
  */
1547
1551
  /** Max runs retained in `deployments/index.json` (newest first). */
1548
1552
  const DEPLOYMENT_EVENTS_MAX_INDEX_RUNS = 20;
@@ -1552,7 +1556,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1552
1556
  const FLUSH_EVENT_THRESHOLD = 50;
1553
1557
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1554
1558
  function getCdkdVersion() {
1555
- return "0.227.1";
1559
+ return "0.228.0";
1556
1560
  }
1557
1561
  /**
1558
1562
  * Generate a time-sortable unique run id, e.g.
@@ -1571,6 +1575,29 @@ function deploymentEventsKey(prefix, stackName, region, runId) {
1571
1575
  function deploymentEventsIndexKey(prefix, stackName, region) {
1572
1576
  return `${prefix}/${stackName}/${region}/deployments/index.json`;
1573
1577
  }
1578
+ /** S3 key prefix under which a stack's `deployments/` artifacts live. */
1579
+ function deploymentsDirPrefix(prefix, stackName, region) {
1580
+ return `${prefix}/${stackName}/${region}/deployments/`;
1581
+ }
1582
+ /**
1583
+ * Parse the wall-clock time encoded in a run id's leading timestamp
1584
+ * (e.g. `20260613T012345678Z-1a2b3c4d`) back to epoch milliseconds.
1585
+ * Returns `null` for any id that does not start with the canonical
1586
+ * compact-ISO prefix — the age-based pruner treats an unparseable id as
1587
+ * "do not delete on age grounds", which is the safe direction.
1588
+ */
1589
+ function runIdTimestampMs(runId) {
1590
+ const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z/.exec(runId);
1591
+ if (!m) return null;
1592
+ const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}.${m[7]}Z`;
1593
+ const ms = Date.parse(iso);
1594
+ return Number.isNaN(ms) ? null : ms;
1595
+ }
1596
+ /** Extract the `{runId}` from a `.../{runId}.jsonl` event-stream key. */
1597
+ function runIdFromJsonlKey(key, dirPrefix) {
1598
+ if (!key.startsWith(dirPrefix) || !key.endsWith(".jsonl")) return null;
1599
+ return key.slice(dirPrefix.length, -6);
1600
+ }
1574
1601
  /**
1575
1602
  * Buffering JSONL writer for one deployment run. Implements the
1576
1603
  * {@link DeploymentEventRecorder} seam the deploy engine / destroy
@@ -1634,7 +1661,8 @@ var DeploymentEventsStore = class {
1634
1661
  if (this.events.length === 0) return;
1635
1662
  await this.enqueueWrite(async () => {
1636
1663
  await this.doFlush();
1637
- await this.updateIndex(result);
1664
+ const keptRunIds = await this.updateIndex(result);
1665
+ await this.pruneSupersededRunFiles(keptRunIds);
1638
1666
  });
1639
1667
  }
1640
1668
  /** Await any in-flight async flushes (used by tests). */
@@ -1671,6 +1699,10 @@ var DeploymentEventsStore = class {
1671
1699
  * the last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs. Read-modify-
1672
1700
  * write WITHOUT optimistic locking — last-writer-wins (documented
1673
1701
  * trade-off; the per-run `.jsonl` files are the source of truth).
1702
+ *
1703
+ * Returns the run ids retained in the index (newest-first), which the
1704
+ * caller feeds to {@link pruneSupersededRunFiles} so the `.jsonl` files
1705
+ * stay bounded to the same window as the index.
1674
1706
  */
1675
1707
  async updateIndex(result) {
1676
1708
  const key = deploymentEventsIndexKey(this.backend.prefix, this.stackName, this.region);
@@ -1701,6 +1733,35 @@ var DeploymentEventsStore = class {
1701
1733
  lastModified: Date.now()
1702
1734
  };
1703
1735
  await this.backend.putRawObject(key, JSON.stringify(file, null, 2));
1736
+ return runs.map((r) => r.runId);
1737
+ }
1738
+ /**
1739
+ * Self-bounding prune (issue #885): delete `{runId}.jsonl` streams that
1740
+ * have fallen out of the index window so the `deployments/` prefix does
1741
+ * not grow without bound. Best-effort — runs inside the same write-chain
1742
+ * link as the flush + index write, so a failure here is caught + warned
1743
+ * once by {@link enqueueWrite} and never blocks the deploy / destroy.
1744
+ *
1745
+ * Concurrency-tolerant by construction: the cutoff is the OLDEST retained
1746
+ * run id, and only `.jsonl` files strictly below it are deleted. Run ids
1747
+ * are time-sortable, so a concurrent NEWER run's id sorts ABOVE every
1748
+ * retained id and is never touched. A concurrent run that STARTED earlier
1749
+ * but is still writing could in theory fall below the cutoff if 20+ newer
1750
+ * runs finalized while it ran — an extreme edge that self-heals anyway,
1751
+ * since the whole JSONL body is re-PUT on that run's next flush / finalize
1752
+ * (S3 has no append; each flush rewrites the full stream).
1753
+ */
1754
+ async pruneSupersededRunFiles(keptRunIds) {
1755
+ if (keptRunIds.length < 20) return;
1756
+ const cutoff = keptRunIds.reduce((min, id) => id < min ? id : min, keptRunIds[0]);
1757
+ const dirPrefix = deploymentsDirPrefix(this.backend.prefix, this.stackName, this.region);
1758
+ const stale = (await this.backend.listRawKeys(dirPrefix)).filter((k) => {
1759
+ const runId = runIdFromJsonlKey(k, dirPrefix);
1760
+ return runId !== null && runId < cutoff;
1761
+ });
1762
+ if (stale.length === 0) return;
1763
+ await this.backend.deleteRawObjects(stale);
1764
+ this.logger.debug(`Pruned ${stale.length} superseded deployment-event stream(s) for ${this.stackName} (${this.region})`);
1704
1765
  }
1705
1766
  warnOnce(message) {
1706
1767
  if (this.warnedOnce) {
@@ -1819,6 +1880,102 @@ var DeploymentEventsReader = class {
1819
1880
  }
1820
1881
  return events;
1821
1882
  }
1883
+ /**
1884
+ * Prune a stack's recorded deployment-event runs (issue #885 — `cdkd
1885
+ * events prune`). Deletes `{runId}.jsonl` streams beyond a retention
1886
+ * window and rewrites (or removes) `index.json` to match.
1887
+ *
1888
+ * Retention semantics (see {@link DeploymentEventsPruneOptions}):
1889
+ * - `all` — delete every run + the index (full purge).
1890
+ * - `keep N` — retain the newest N runs, delete the rest.
1891
+ * - `olderThanMs`— delete runs whose run-id timestamp is older than the
1892
+ * cutoff; a run id with no parseable timestamp is kept.
1893
+ * - `keep` + `olderThanMs` — a run is deleted only when it is BOTH
1894
+ * beyond the newest-N window AND older than the cutoff
1895
+ * (the most conservative combination).
1896
+ * - none of the above — defaults to `keep DEPLOYMENT_EVENTS_MAX_INDEX_RUNS`
1897
+ * (matches the index window the writer self-bounds to).
1898
+ *
1899
+ * Unlike the writer's best-effort auto-prune, this is user-initiated and
1900
+ * surfaces errors to the caller (the command reports / exits non-zero).
1901
+ */
1902
+ async pruneRuns(stackName, region, opts = {}) {
1903
+ const dirPrefix = deploymentsDirPrefix(this.backend.prefix, stackName, region);
1904
+ const runIdsDesc = (await this.backend.listRawKeys(dirPrefix)).map((k) => runIdFromJsonlKey(k, dirPrefix)).filter((id) => id !== null).sort().reverse();
1905
+ const indexKey = deploymentEventsIndexKey(this.backend.prefix, stackName, region);
1906
+ if (opts.all === true) {
1907
+ const toDelete = runIdsDesc.map((id) => deploymentEventsKey(this.backend.prefix, stackName, region, id));
1908
+ await this.backend.deleteRawObjects([...toDelete, indexKey]);
1909
+ return {
1910
+ deletedRunIds: runIdsDesc,
1911
+ remainingRunIds: [],
1912
+ indexDeleted: true
1913
+ };
1914
+ }
1915
+ const noGuards = opts.keep === void 0 && opts.olderThanMs === void 0;
1916
+ const keep = opts.keep ?? (noGuards ? 20 : void 0);
1917
+ const protectedByCount = keep === void 0 ? /* @__PURE__ */ new Set() : new Set(runIdsDesc.slice(0, keep));
1918
+ let candidates = runIdsDesc.filter((id) => !protectedByCount.has(id));
1919
+ if (opts.olderThanMs !== void 0) {
1920
+ const cutoff = (opts.now ?? /* @__PURE__ */ new Date()).getTime() - opts.olderThanMs;
1921
+ candidates = candidates.filter((id) => {
1922
+ const t = runIdTimestampMs(id);
1923
+ return t !== null && t < cutoff;
1924
+ });
1925
+ }
1926
+ if (candidates.length === 0) return {
1927
+ deletedRunIds: [],
1928
+ remainingRunIds: runIdsDesc,
1929
+ indexDeleted: false
1930
+ };
1931
+ const deletedSet = new Set(candidates);
1932
+ const deleteKeys = candidates.map((id) => deploymentEventsKey(this.backend.prefix, stackName, region, id));
1933
+ const remainingRunIds = runIdsDesc.filter((id) => !deletedSet.has(id));
1934
+ const indexDeleted = await this.rewriteIndexAfterPrune(indexKey, stackName, region, deletedSet, remainingRunIds.length === 0);
1935
+ await this.backend.deleteRawObjects(deleteKeys);
1936
+ return {
1937
+ deletedRunIds: candidates,
1938
+ remainingRunIds,
1939
+ indexDeleted
1940
+ };
1941
+ }
1942
+ /**
1943
+ * Drop the pruned run ids from `index.json`, or delete the index entirely
1944
+ * when no `.jsonl` streams remain. A corrupt / unreadable index is left
1945
+ * untouched (the `.jsonl` files are the source of truth; `cdkd events`
1946
+ * falls back to key enumeration). Returns whether the index was deleted.
1947
+ */
1948
+ async rewriteIndexAfterPrune(indexKey, stackName, region, deletedRunIds, noRunsRemain) {
1949
+ if (noRunsRemain) {
1950
+ await this.backend.deleteRawObjects([indexKey]);
1951
+ return true;
1952
+ }
1953
+ let raw;
1954
+ try {
1955
+ raw = await this.backend.getRawObject(indexKey);
1956
+ } catch {
1957
+ return false;
1958
+ }
1959
+ if (raw === null) return false;
1960
+ let parsed;
1961
+ try {
1962
+ parsed = JSON.parse(raw);
1963
+ } catch {
1964
+ return false;
1965
+ }
1966
+ if (!Array.isArray(parsed.runs)) return false;
1967
+ const remaining = parsed.runs.filter((r) => !deletedRunIds.has(r.runId));
1968
+ if (remaining.length === parsed.runs.length) return false;
1969
+ const file = {
1970
+ indexVersion: 1,
1971
+ stackName,
1972
+ region,
1973
+ runs: remaining,
1974
+ lastModified: Date.now()
1975
+ };
1976
+ await this.backend.putRawObject(indexKey, JSON.stringify(file, null, 2));
1977
+ return false;
1978
+ }
1822
1979
  };
1823
1980
 
1824
1981
  //#endregion
@@ -37350,7 +37507,7 @@ async function retireCloudFormationStack(options) {
37350
37507
  }
37351
37508
  } else ({body: newBody, modified, format} = injectRetainPolicies(tpl.TemplateBody, cfnStackName));
37352
37509
  if (!yes) {
37353
- if (!await confirmPrompt$5(`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`)) {
37510
+ if (!await confirmPrompt$6(`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`)) {
37354
37511
  logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
37355
37512
  for (const cleanup of nestedCleanups) try {
37356
37513
  await cleanup();
@@ -37727,7 +37884,7 @@ async function walkCfnStackTree(stackName, physicalId, cfnClient) {
37727
37884
  nested
37728
37885
  };
37729
37886
  }
37730
- async function confirmPrompt$5(prompt) {
37887
+ async function confirmPrompt$6(prompt) {
37731
37888
  const rl = readline$1.createInterface({
37732
37889
  input: process.stdin,
37733
37890
  output: process.stdout
@@ -38896,7 +39053,7 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
38896
39053
  return;
38897
39054
  }
38898
39055
  if (!options.yes) {
38899
- if (!await confirmPrompt$4(`Update cdkd state with the AWS-current values shown above?`)) {
39056
+ if (!await confirmPrompt$5(`Update cdkd state with the AWS-current values shown above?`)) {
38900
39057
  logger.info("Aborted.");
38901
39058
  return;
38902
39059
  }
@@ -39022,7 +39179,7 @@ async function runRevert(reports, providerRegistry, stateConfig, awsClients, opt
39022
39179
  return;
39023
39180
  }
39024
39181
  if (!options.yes) {
39025
- if (!await confirmPrompt$4(`Push cdkd state values back into AWS for the resources shown above?`)) {
39182
+ if (!await confirmPrompt$5(`Push cdkd state values back into AWS for the resources shown above?`)) {
39026
39183
  logger.info("Aborted.");
39027
39184
  return;
39028
39185
  }
@@ -39130,7 +39287,7 @@ async function runWithConcurrency(tasks, concurrency) {
39130
39287
  })());
39131
39288
  await Promise.all(workers);
39132
39289
  }
39133
- async function confirmPrompt$4(prompt) {
39290
+ async function confirmPrompt$5(prompt) {
39134
39291
  const rl = readline$1.createInterface({
39135
39292
  input: process.stdin,
39136
39293
  output: process.stdout
@@ -39592,6 +39749,86 @@ async function resolveEventsRegion(reader, stackName, explicitRegion) {
39592
39749
  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");
39593
39750
  return regions[0];
39594
39751
  }
39752
+ /**
39753
+ * `cdkd events prune <stack>` — reclaim S3 space by deleting old per-run
39754
+ * `{runId}.jsonl` event streams (issue #885). `cdkd destroy` deliberately
39755
+ * keeps event history as post-mortem context, so this is the explicit way
39756
+ * to purge it; the deploy/destroy writer also self-bounds to the last
39757
+ * {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
39758
+ *
39759
+ * Retention selection:
39760
+ * - `--all` purge every run + the index.
39761
+ * - `--keep <N>` retain the newest N runs.
39762
+ * - `--older-than <dur>` delete runs older than the duration.
39763
+ * - both keep+older-than: delete runs that are BOTH beyond newest-N AND
39764
+ * older than the cutoff.
39765
+ * - none of the above: default to keeping the newest
39766
+ * {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS}.
39767
+ */
39768
+ async function eventsPruneCommand(stackName, options) {
39769
+ const logger = getLogger();
39770
+ if (options.verbose) logger.setLevel("debug");
39771
+ warnIfDeprecatedRegion(options);
39772
+ if (options.all === true && (options.keep !== void 0 || options.olderThan !== void 0)) throw new CdkdError("'--all' purges every run and cannot be combined with '--keep' / '--older-than'.", "EVENTS_PRUNE_BAD_FLAGS");
39773
+ const olderThanMs = options.olderThan !== void 0 ? parseDuration(options.olderThan) : void 0;
39774
+ const awsClients = new AwsClients({
39775
+ ...options.region && { region: options.region },
39776
+ ...options.profile && { profile: options.profile }
39777
+ });
39778
+ setAwsClients(awsClients);
39779
+ try {
39780
+ const region = options.region || process.env["AWS_REGION"] || "us-east-1";
39781
+ const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
39782
+ const prefix = options.statePrefix ?? "cdkd";
39783
+ const stateBackend = new S3StateBackend(awsClients.s3, {
39784
+ bucket,
39785
+ prefix
39786
+ }, {
39787
+ region,
39788
+ ...options.profile && { profile: options.profile }
39789
+ });
39790
+ await stateBackend.verifyBucketExists();
39791
+ const reader = new DeploymentEventsReader(stateBackend);
39792
+ const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
39793
+ const totalRuns = (await reader.listRuns(stackName, targetRegion)).length;
39794
+ const scope = options.all ? `ALL ${totalRuns} run(s)` : options.keep !== void 0 && olderThanMs !== void 0 ? `runs beyond the newest ${options.keep} AND older than ${options.olderThan}` : options.keep !== void 0 ? `runs beyond the newest ${options.keep}` : olderThanMs !== void 0 ? `runs older than ${options.olderThan}` : `runs beyond the newest ${20}`;
39795
+ if (options.yes !== true) {
39796
+ if (!process.stdin.isTTY) {
39797
+ logger.info(gray(`Refusing to prune deployment-event history for ${stackName} (${targetRegion}) without confirmation on a non-interactive terminal. Re-run with --yes to proceed.`));
39798
+ return;
39799
+ }
39800
+ if (!await confirmPrompt$4(`Prune deployment-event history for ${cyan(stackName)} ${gray(`(${targetRegion})`)}: ${scope}?`)) {
39801
+ logger.info(gray("Aborted; nothing was deleted."));
39802
+ return;
39803
+ }
39804
+ }
39805
+ const result = await reader.pruneRuns(stackName, targetRegion, {
39806
+ ...options.all === true && { all: true },
39807
+ ...options.keep !== void 0 && { keep: options.keep },
39808
+ ...olderThanMs !== void 0 && { olderThanMs }
39809
+ });
39810
+ if (result.deletedRunIds.length === 0) {
39811
+ logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${stackName} (${targetRegion}); no run streams to delete.` : `No runs matched the prune criteria for ${stackName} (${targetRegion}).`));
39812
+ return;
39813
+ }
39814
+ logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(stackName)} ${gray(`(${targetRegion})`)}; ${result.remainingRunIds.length} retained` + (result.indexDeleted ? gray(" (index removed)") : "") + ".");
39815
+ } finally {
39816
+ awsClients.destroy();
39817
+ }
39818
+ }
39819
+ /** Minimal `(y/N)` confirmation prompt. */
39820
+ async function confirmPrompt$4(prompt) {
39821
+ const rl = readline$1.createInterface({
39822
+ input: process.stdin,
39823
+ output: process.stdout
39824
+ });
39825
+ try {
39826
+ const ans = await rl.question(`${prompt} [y/N] `);
39827
+ return /^y(es)?$/i.test(ans.trim());
39828
+ } finally {
39829
+ rl.close();
39830
+ }
39831
+ }
39595
39832
  /** Human-readable run listing (newest first). */
39596
39833
  function printRunList(stackName, region, runs) {
39597
39834
  const logger = getLogger();
@@ -39652,8 +39889,27 @@ function createEventsCommand() {
39652
39889
  }));
39653
39890
  [...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
39654
39891
  cmd.addOption(deprecatedRegionOption);
39892
+ cmd.addCommand(createEventsPruneCommand());
39655
39893
  return cmd;
39656
39894
  }
39895
+ /**
39896
+ * Create the `cdkd events prune <stack>` subcommand (issue #885).
39897
+ *
39898
+ * Only the prune-SPECIFIC options (`--keep` / `--older-than` / `--all`) are
39899
+ * declared here. The shared option blocks (`commonOptions` + `stateOptions`
39900
+ * + the deprecated region option) and `--stack-region` are inherited from
39901
+ * the parent `events` command — declaring the same flag on BOTH parent and
39902
+ * child makes Commander route a post-subcommand flag (`events prune X --yes`)
39903
+ * to the PARENT's storage, leaving the child's value at its default. The
39904
+ * action therefore reads the merged view via `command.optsWithGlobals()`.
39905
+ */
39906
+ function createEventsPruneCommand() {
39907
+ return new Command("prune").description("Delete old per-run deployment-event streams to reclaim S3 space").argument("<stack>", "Stack name (physical CloudFormation name)").addOption(new Option("--keep <N>", "Retain only the newest N runs").argParser((v) => {
39908
+ const n = parseInt(v, 10);
39909
+ if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${v}": expected a non-negative integer.`);
39910
+ return n;
39911
+ })).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index (full purge)", false).action(withErrorHandling((stack, _options, command) => eventsPruneCommand(stack, command.optsWithGlobals())));
39912
+ }
39657
39913
 
39658
39914
  //#endregion
39659
39915
  //#region src/cli/cdk-path.ts
@@ -54696,7 +54952,7 @@ function reorderArgs(argv) {
54696
54952
  async function main() {
54697
54953
  installPipeCloseHandler();
54698
54954
  const program = new Command();
54699
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.227.1");
54955
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.228.0");
54700
54956
  program.addCommand(createBootstrapCommand());
54701
54957
  program.addCommand(createSynthCommand());
54702
54958
  program.addCommand(createListCommand());