@treeseed/sdk 0.10.15 → 0.10.17

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.
@@ -1,10 +1,24 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, relative, resolve } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { resolveTreeseedWebCachePolicy } from "../../platform/deploy-config.js";
7
- import { normalizeRailwayEnvironmentName } from "./railway-api.js";
7
+ import {
8
+ deleteRailwayCustomDomain,
9
+ deleteRailwayEnvironment,
10
+ deleteRailwayProject,
11
+ deleteRailwayVolume,
12
+ getRailwayServiceInstance,
13
+ listRailwayCustomDomains,
14
+ listRailwayProjects,
15
+ listRailwayVariables,
16
+ listRailwayVolumes,
17
+ normalizeRailwayEnvironmentName,
18
+ resolveRailwayApiToken,
19
+ resolveRailwayWorkspace,
20
+ resolveRailwayWorkspaceContext
21
+ } from "./railway-api.js";
8
22
  import { loadCliDeployConfig, resolveWranglerBin } from "./runtime-tools.js";
9
23
  import { sdkD1MigrationsRoot } from "./runtime-paths.js";
10
24
  const DEFAULT_COMPATIBILITY_DATE = "2026-04-05";
@@ -252,11 +266,22 @@ function targetWorkersDevUrl(workerName) {
252
266
  function relativeFromGeneratedRoot(targetPath, generatedRoot) {
253
267
  return relative(generatedRoot, targetPath).replaceAll("\\", "/");
254
268
  }
255
- function buildPublicVars(deployConfig) {
269
+ function resolveContentServingMode(deployConfig, options = {}) {
270
+ const override = envOrNull("TREESEED_CONTENT_SERVING_MODE");
271
+ if (override) {
272
+ return override;
273
+ }
274
+ const target = options.target ? normalizeTarget(options.target) : null;
275
+ if (target?.kind === "persistent" && target.scope !== "local") {
276
+ return "published_runtime";
277
+ }
278
+ return deployConfig.providers?.content?.serving ?? "local_collections";
279
+ }
280
+ function buildPublicVars(deployConfig, options = {}) {
256
281
  const identity = resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget("prod"));
257
282
  const contentRuntimeProvider = deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay";
258
283
  const contentPublishProvider = deployConfig.providers?.content?.publish ?? contentRuntimeProvider;
259
- const contentServingMode = envOrNull("TREESEED_CONTENT_SERVING_MODE") ?? deployConfig.providers?.content?.serving ?? "local_collections";
284
+ const contentServingMode = resolveContentServingMode(deployConfig, options);
260
285
  const contentDefaultTeamId = identity.teamId;
261
286
  const contentManifestKeyTemplate = deployConfig.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json";
262
287
  const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
@@ -751,7 +776,7 @@ function buildWranglerConfigContents(tenantRoot, deployConfig, state, options =
751
776
  const assetsDirectory = relativeFromGeneratedRoot(resolve(tenantRoot, "dist"), generatedRoot);
752
777
  const migrationsDir = relativeFromGeneratedRoot(sdkD1MigrationsRoot, generatedRoot);
753
778
  const vars = {
754
- ...buildPublicVars(deployConfig),
779
+ ...buildPublicVars(deployConfig, { target }),
755
780
  ...buildLocalRuntimeVars(deployConfig, state, target, options.env)
756
781
  };
757
782
  const r2Config = deployConfig.cloudflare.r2;
@@ -1645,7 +1670,7 @@ function resolveExistingD1ByName(d1Databases, expectedName, current) {
1645
1670
  };
1646
1671
  }
1647
1672
  function looksLikeMissingResource(output) {
1648
- return /not found|does not exist|could not find|unknown/i.test(output);
1673
+ return /not found|does not exist|could(?: not|n't) find|couldnt find/i.test(output);
1649
1674
  }
1650
1675
  function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = false }) {
1651
1676
  if (!namespaceId || isPlaceholderResourceId(namespaceId)) {
@@ -1716,7 +1741,661 @@ ${result.stderr ?? ""}`;
1716
1741
  }
1717
1742
  return { status: result.status === 0 ? "deleted" : "missing", name: workerName };
1718
1743
  }
1719
- function destroyCloudflareResources(tenantRoot, options = {}) {
1744
+ function resourceOperation(provider, type, name, status, extra = {}) {
1745
+ return {
1746
+ provider,
1747
+ type,
1748
+ name: name ?? null,
1749
+ status,
1750
+ ...extra
1751
+ };
1752
+ }
1753
+ function deleteCloudflareApiResource(path, { env, dryRun, name, type }) {
1754
+ if (!path) {
1755
+ return resourceOperation("cloudflare", type, name, "missing");
1756
+ }
1757
+ if (dryRun) {
1758
+ return resourceOperation("cloudflare", type, name, "planned", { path });
1759
+ }
1760
+ const result = cloudflareApiRequest(path, { method: "DELETE", env, allowFailure: true });
1761
+ if (result?.success === false && !looksLikeMissingResource(formatCloudflareErrors(result))) {
1762
+ throw new Error(formatCloudflareErrors(result) || `Failed to delete Cloudflare ${type} ${name}.`);
1763
+ }
1764
+ return resourceOperation("cloudflare", type, name, result?.success === false ? "missing" : "deleted", { path });
1765
+ }
1766
+ function formatCloudflareErrors(payload) {
1767
+ return Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).filter(Boolean).join("; ") : "";
1768
+ }
1769
+ function deleteQueueByName(tenantRoot, queue, { env, dryRun }) {
1770
+ const name = queueName(queue) ?? queue?.name ?? null;
1771
+ const id = queueId(queue);
1772
+ if (!name) {
1773
+ return resourceOperation("cloudflare", "queue", name, "missing");
1774
+ }
1775
+ if (dryRun) {
1776
+ return resourceOperation("cloudflare", "queue", name, "planned", { id });
1777
+ }
1778
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1779
+ const path = id ? `/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(id)}` : null;
1780
+ if (path) {
1781
+ const deleted = deleteCloudflareApiResource(path, { env, dryRun: false, name, type: "queue" });
1782
+ if (deleted.status === "deleted" || deleted.status === "missing") {
1783
+ return { ...deleted, id };
1784
+ }
1785
+ }
1786
+ const result = runWrangler(["queues", "delete", name], {
1787
+ cwd: tenantRoot,
1788
+ allowFailure: true,
1789
+ capture: true,
1790
+ env,
1791
+ input: "y\n"
1792
+ });
1793
+ const output = `${result.stdout ?? ""}
1794
+ ${result.stderr ?? ""}`;
1795
+ if (result.status !== 0 && !looksLikeMissingResource(output)) {
1796
+ throw new Error(output.trim() || `Failed to delete queue ${name}.`);
1797
+ }
1798
+ return resourceOperation("cloudflare", "queue", name, result.status === 0 ? "deleted" : "missing", { id });
1799
+ }
1800
+ function isLegacyTreeseedQueueName(name, scope) {
1801
+ if (!name || !scope) {
1802
+ return false;
1803
+ }
1804
+ const environmentNames = scope === "prod" ? ["prod", "production"] : [scope];
1805
+ return environmentNames.some(
1806
+ (environmentName) => name === `agent-work-${environmentName}` || name === `agent-work-dlq-${environmentName}`
1807
+ );
1808
+ }
1809
+ function legacyQueueDestroyOperations(tenantRoot, queues, target, knownNames, { env, dryRun }) {
1810
+ const scope = target.kind === "persistent" ? target.scope : target.branchName;
1811
+ return queues.filter((queue) => {
1812
+ const name = queueName(queue);
1813
+ return name && !knownNames.has(name) && isLegacyTreeseedQueueName(name, scope);
1814
+ }).map((queue) => {
1815
+ const deleted = deleteQueueByName(tenantRoot, queue, { env, dryRun });
1816
+ return { ...deleted, legacy: true };
1817
+ });
1818
+ }
1819
+ function deleteR2Bucket(tenantRoot, bucketName, { env, dryRun, deleteData }) {
1820
+ if (!bucketName) {
1821
+ return resourceOperation("cloudflare", "r2-bucket", bucketName, "missing");
1822
+ }
1823
+ if (!deleteData) {
1824
+ return resourceOperation("cloudflare", "r2-bucket", bucketName, "skipped", { reason: "data_preserved" });
1825
+ }
1826
+ if (dryRun) {
1827
+ return resourceOperation("cloudflare", "r2-bucket", bucketName, "planned");
1828
+ }
1829
+ const drained = drainR2Bucket(bucketName, { env });
1830
+ const result = runWrangler(["r2", "bucket", "delete", bucketName], {
1831
+ cwd: tenantRoot,
1832
+ allowFailure: true,
1833
+ capture: true,
1834
+ env,
1835
+ input: "y\n"
1836
+ });
1837
+ const output = `${result.stdout ?? ""}
1838
+ ${result.stderr ?? ""}`;
1839
+ if (result.status !== 0 && !looksLikeMissingResource(output)) {
1840
+ throw new Error(output.trim() || `Failed to delete R2 bucket ${bucketName}.`);
1841
+ }
1842
+ return resourceOperation("cloudflare", "r2-bucket", bucketName, result.status === 0 ? "deleted" : "missing", drained);
1843
+ }
1844
+ function r2ObjectKey(entry) {
1845
+ return typeof entry?.key === "string" ? entry.key : typeof entry?.name === "string" ? entry.name : "";
1846
+ }
1847
+ function listR2Objects(bucketName, { env }) {
1848
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1849
+ if (!accountId || !bucketName) {
1850
+ return [];
1851
+ }
1852
+ const objects = [];
1853
+ let cursor = "";
1854
+ for (let page = 0; page < 100; page += 1) {
1855
+ const payload = cloudflareApiRequest(
1856
+ `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects?per_page=1000${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
1857
+ { env, allowFailure: true }
1858
+ );
1859
+ if (payload?.success === false) {
1860
+ break;
1861
+ }
1862
+ const pageObjects = Array.isArray(payload?.result) ? payload.result : Array.isArray(payload?.result?.objects) ? payload.result.objects : [];
1863
+ objects.push(...pageObjects);
1864
+ const nextCursor = typeof payload?.result_info?.cursor === "string" ? payload.result_info.cursor : typeof payload?.result?.cursor === "string" ? payload.result.cursor : "";
1865
+ if (!nextCursor || nextCursor === cursor || pageObjects.length === 0) {
1866
+ break;
1867
+ }
1868
+ cursor = nextCursor;
1869
+ }
1870
+ return objects;
1871
+ }
1872
+ function drainR2Bucket(bucketName, { env }) {
1873
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1874
+ if (!accountId || !bucketName) {
1875
+ return { objectsDeleted: 0 };
1876
+ }
1877
+ let objectsDeleted = 0;
1878
+ for (let batch = 0; batch < 100; batch += 1) {
1879
+ const objects = listR2Objects(bucketName, { env });
1880
+ if (objects.length === 0) {
1881
+ break;
1882
+ }
1883
+ let batchDeleted = 0;
1884
+ for (const object of objects) {
1885
+ const key = r2ObjectKey(object);
1886
+ if (!key) {
1887
+ continue;
1888
+ }
1889
+ const result = cloudflareApiRequest(
1890
+ `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(key)}`,
1891
+ { method: "DELETE", env, allowFailure: true }
1892
+ );
1893
+ if (result?.success === false) {
1894
+ const message = formatCloudflareErrors(result);
1895
+ if (looksLikeMissingResource(message)) {
1896
+ continue;
1897
+ }
1898
+ throw new Error(message || `Failed to delete R2 object ${key}.`);
1899
+ }
1900
+ objectsDeleted += 1;
1901
+ batchDeleted += 1;
1902
+ }
1903
+ if (batchDeleted === 0) {
1904
+ break;
1905
+ }
1906
+ }
1907
+ return { objectsDeleted };
1908
+ }
1909
+ function deleteD1DatabaseForDestroy(tenantRoot, databaseName, { env, dryRun, deleteData }) {
1910
+ if (!deleteData) {
1911
+ return resourceOperation("cloudflare", "d1-database", databaseName, "skipped", { reason: "data_preserved" });
1912
+ }
1913
+ const result = deleteD1Database(tenantRoot, databaseName, { env, dryRun });
1914
+ return resourceOperation("cloudflare", "d1-database", databaseName, result.status, result);
1915
+ }
1916
+ function pagesDomainName(domain) {
1917
+ return typeof domain?.name === "string" ? domain.name : typeof domain?.domain === "string" ? domain.domain : typeof domain?.hostname === "string" ? domain.hostname : "";
1918
+ }
1919
+ function listPagesCustomDomains(projectName, { env }) {
1920
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1921
+ if (!projectName || !accountId) {
1922
+ return [];
1923
+ }
1924
+ const domains = [];
1925
+ let page = 1;
1926
+ let totalPages = 1;
1927
+ while (page <= totalPages && page <= 50) {
1928
+ const payload = cloudflareApiRequest(
1929
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains?per_page=100&page=${page}`,
1930
+ { env, allowFailure: true }
1931
+ );
1932
+ if (payload?.success === false) {
1933
+ break;
1934
+ }
1935
+ if (Array.isArray(payload?.result)) {
1936
+ domains.push(...payload.result);
1937
+ }
1938
+ const reportedTotal = Number(payload?.result_info?.total_pages);
1939
+ totalPages = Number.isFinite(reportedTotal) && reportedTotal > 0 ? reportedTotal : page;
1940
+ page += 1;
1941
+ }
1942
+ return domains;
1943
+ }
1944
+ function listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env }) {
1945
+ const result = runWrangler(["pages", "project", "list", "--json"], {
1946
+ cwd: tenantRoot,
1947
+ allowFailure: true,
1948
+ capture: true,
1949
+ env
1950
+ });
1951
+ if (result.status !== 0) {
1952
+ return [];
1953
+ }
1954
+ try {
1955
+ const projects = JSON.parse(result.stdout || "[]");
1956
+ const project = (Array.isArray(projects) ? projects : []).find((entry) => entry?.name === projectName || entry?.projectName === projectName || entry?.["Project Name"] === projectName);
1957
+ const domains = typeof project?.["Project Domains"] === "string" ? project["Project Domains"] : typeof project?.domains === "string" ? project.domains : "";
1958
+ return domains.split(",").map((entry) => entry.trim()).filter((entry) => entry && !entry.endsWith(".pages.dev"));
1959
+ } catch {
1960
+ return [];
1961
+ }
1962
+ }
1963
+ function deletePagesCustomDomains(tenantRoot, projectName, knownNames, { env, dryRun }) {
1964
+ if (!projectName) {
1965
+ return [resourceOperation("cloudflare", "pages-custom-domain", projectName, "missing")];
1966
+ }
1967
+ const desiredNames = [...new Set((knownNames ?? []).filter(Boolean))];
1968
+ if (dryRun) {
1969
+ return desiredNames.length > 0 ? desiredNames.map((name) => resourceOperation("cloudflare", "pages-custom-domain", name, "planned", { projectName })) : [resourceOperation("cloudflare", "pages-custom-domain", projectName, "planned", { reason: "project_delete_prerequisite" })];
1970
+ }
1971
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1972
+ if (!accountId) {
1973
+ return desiredNames.length > 0 ? desiredNames.map((name) => resourceOperation("cloudflare", "pages-custom-domain", name, "blocked", { projectName, reason: "missing_cloudflare_account_id" })) : [resourceOperation("cloudflare", "pages-custom-domain", projectName, "blocked", { reason: "missing_cloudflare_account_id" })];
1974
+ }
1975
+ const listedNames = listPagesCustomDomains(projectName, { env }).map(pagesDomainName).filter(Boolean);
1976
+ const wranglerNames = listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env });
1977
+ const domainNames = [.../* @__PURE__ */ new Set([...desiredNames, ...listedNames, ...wranglerNames])];
1978
+ if (domainNames.length === 0) {
1979
+ return [resourceOperation("cloudflare", "pages-custom-domain", projectName, "missing", { projectName })];
1980
+ }
1981
+ return domainNames.map((name) => deleteCloudflareApiResource(
1982
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains/${encodeURIComponent(name)}`,
1983
+ { env, dryRun: false, name, type: "pages-custom-domain" }
1984
+ ));
1985
+ }
1986
+ function normalizePagesDeploymentId(deployment) {
1987
+ return typeof deployment?.id === "string" ? deployment.id : typeof deployment?.Id === "string" ? deployment.Id : "";
1988
+ }
1989
+ function normalizePagesDeployments(value) {
1990
+ return (Array.isArray(value) ? value : Array.isArray(value?.result) ? value.result : []).filter((entry) => normalizePagesDeploymentId(entry));
1991
+ }
1992
+ function listPagesDeploymentsWithApi(projectName, { env }) {
1993
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1994
+ if (!projectName || !accountId) {
1995
+ return [];
1996
+ }
1997
+ const deployments = [];
1998
+ for (const pagesEnvironment of ["preview", "production"]) {
1999
+ let page = 1;
2000
+ let totalPages = 1;
2001
+ while (page <= totalPages && page <= 50) {
2002
+ const payload = cloudflareApiRequest(
2003
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/deployments?per_page=100&page=${page}&env=${pagesEnvironment}`,
2004
+ { env, allowFailure: true }
2005
+ );
2006
+ if (payload?.success === false) {
2007
+ break;
2008
+ }
2009
+ deployments.push(...normalizePagesDeployments(payload));
2010
+ const reportedTotal = Number(payload?.result_info?.total_pages);
2011
+ totalPages = Number.isFinite(reportedTotal) && reportedTotal > 0 ? reportedTotal : page;
2012
+ page += 1;
2013
+ }
2014
+ }
2015
+ return deployments;
2016
+ }
2017
+ function listPagesDeployments(tenantRoot, projectName, { env }) {
2018
+ const deployments = [];
2019
+ for (const pagesEnvironment of ["preview", "production"]) {
2020
+ const result = runWrangler(["pages", "deployment", "list", "--project-name", projectName, "--environment", pagesEnvironment, "--json"], {
2021
+ cwd: tenantRoot,
2022
+ allowFailure: true,
2023
+ capture: true,
2024
+ env
2025
+ });
2026
+ if (result.status !== 0) {
2027
+ continue;
2028
+ }
2029
+ try {
2030
+ deployments.push(...normalizePagesDeployments(JSON.parse(result.stdout || "[]")));
2031
+ } catch {
2032
+ }
2033
+ }
2034
+ if (deployments.length > 0) {
2035
+ const byId = new Map(deployments.map((deployment) => [normalizePagesDeploymentId(deployment), deployment]));
2036
+ return [...byId.values()];
2037
+ }
2038
+ return listPagesDeploymentsWithApi(projectName, { env });
2039
+ }
2040
+ function deletePagesDeployments(tenantRoot, projectName, { env, dryRun }) {
2041
+ if (!projectName) {
2042
+ return resourceOperation("cloudflare", "pages-deployments", projectName, "missing");
2043
+ }
2044
+ if (dryRun) {
2045
+ return resourceOperation("cloudflare", "pages-deployments", projectName, "planned", { reason: "project_delete_prerequisite" });
2046
+ }
2047
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
2048
+ if (!accountId) {
2049
+ return resourceOperation("cloudflare", "pages-deployments", projectName, "blocked", { reason: "missing_cloudflare_account_id" });
2050
+ }
2051
+ let deleted = 0;
2052
+ let skipped = 0;
2053
+ let total = 0;
2054
+ for (let batch = 0; batch < 100; batch += 1) {
2055
+ const deployments = listPagesDeployments(tenantRoot, projectName, { env });
2056
+ if (deployments.length === 0) {
2057
+ return resourceOperation("cloudflare", "pages-deployments", projectName, deleted > 0 ? "deleted" : "missing", {
2058
+ deleted,
2059
+ skipped,
2060
+ total
2061
+ });
2062
+ }
2063
+ total += deployments.length;
2064
+ let batchDeleted = 0;
2065
+ let batchSkipped = 0;
2066
+ for (const deployment of deployments) {
2067
+ const deploymentId = normalizePagesDeploymentId(deployment);
2068
+ if (!deploymentId) {
2069
+ continue;
2070
+ }
2071
+ const result = cloudflareApiRequest(
2072
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/deployments/${encodeURIComponent(deploymentId)}`,
2073
+ { method: "DELETE", env, allowFailure: true }
2074
+ );
2075
+ if (result?.success === false) {
2076
+ const message = formatCloudflareErrors(result);
2077
+ if (/active production deployment|production deployment|deployment is aliased|aliased deployment/iu.test(message)) {
2078
+ skipped += 1;
2079
+ batchSkipped += 1;
2080
+ continue;
2081
+ }
2082
+ if (looksLikeMissingResource(message)) {
2083
+ continue;
2084
+ }
2085
+ throw new Error(message || `Failed to delete Pages deployment ${deploymentId}.`);
2086
+ }
2087
+ deleted += 1;
2088
+ batchDeleted += 1;
2089
+ }
2090
+ if (batchDeleted === 0 && batchSkipped >= deployments.length) {
2091
+ break;
2092
+ }
2093
+ }
2094
+ return resourceOperation("cloudflare", "pages-deployments", projectName, deleted > 0 ? "deleted" : "skipped", {
2095
+ deleted,
2096
+ skipped,
2097
+ total
2098
+ });
2099
+ }
2100
+ function deletePagesProject(projectName, { env, dryRun }) {
2101
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
2102
+ if (!projectName || !accountId) {
2103
+ return resourceOperation("cloudflare", "pages-project", projectName, "missing");
2104
+ }
2105
+ return deleteCloudflareApiResource(
2106
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}`,
2107
+ { env, dryRun, name: projectName, type: "pages-project" }
2108
+ );
2109
+ }
2110
+ function listDnsRecordsForName(zoneId, name, env) {
2111
+ if (!zoneId || !name) {
2112
+ return [];
2113
+ }
2114
+ const result = cloudflareApiRequest(`/zones/${encodeURIComponent(zoneId)}/dns_records?name=${encodeURIComponent(name)}&per_page=100`, { env, allowFailure: true });
2115
+ return Array.isArray(result?.result) ? result.result : [];
2116
+ }
2117
+ function deleteDnsRecordsForName(deployConfig, name, { env, dryRun }) {
2118
+ if (!name) {
2119
+ return [resourceOperation("cloudflare", "dns-record", name, "missing")];
2120
+ }
2121
+ if (dryRun) {
2122
+ return [resourceOperation("cloudflare", "dns-record", name, "planned")];
2123
+ }
2124
+ const zoneId = resolveCloudflareZoneIdForHost(deployConfig, name, env);
2125
+ if (!zoneId) {
2126
+ return [resourceOperation("cloudflare", "dns-record", name, "blocked", { reason: "zone_unresolved" })];
2127
+ }
2128
+ const records = listDnsRecordsForName(zoneId, name, env);
2129
+ if (records.length === 0) {
2130
+ return [resourceOperation("cloudflare", "dns-record", name, "missing", { zoneId })];
2131
+ }
2132
+ return records.map((record) => {
2133
+ const recordName = record?.name ?? name;
2134
+ return deleteCloudflareApiResource(
2135
+ `/zones/${encodeURIComponent(zoneId)}/dns_records/${encodeURIComponent(record.id)}`,
2136
+ { env, dryRun: false, name: recordName, type: "dns-record" }
2137
+ );
2138
+ });
2139
+ }
2140
+ function deleteTreeseedCacheRules(deployConfig, state, { env, dryRun }) {
2141
+ const targets = [
2142
+ { role: "web", zoneId: state.webCache?.webZoneId, host: state.webCache?.webHost },
2143
+ { role: "content", zoneId: state.webCache?.contentZoneId, host: state.webCache?.contentHost }
2144
+ ].filter((entry) => entry.host || entry.zoneId);
2145
+ if (targets.length === 0) {
2146
+ return [resourceOperation("cloudflare", "cache-rules", null, "missing")];
2147
+ }
2148
+ return targets.map((target) => {
2149
+ const zoneId = target.zoneId ?? resolveCloudflareZoneIdForHost(deployConfig, target.host, env);
2150
+ if (!zoneId) {
2151
+ return resourceOperation("cloudflare", "cache-rules", target.host, "blocked", { reason: "zone_unresolved" });
2152
+ }
2153
+ if (dryRun) {
2154
+ return resourceOperation("cloudflare", "cache-rules", target.host, "planned", { zoneId });
2155
+ }
2156
+ const rulesets = listCloudflareZoneRulesets(zoneId, env);
2157
+ const ruleset = rulesets.find((entry) => entry?.phase === "http_request_cache_settings") ?? null;
2158
+ const rules = Array.isArray(ruleset?.rules) ? ruleset.rules : [];
2159
+ const kept = rules.filter((rule) => typeof rule?.description !== "string" || !rule.description.startsWith("treeseed-managed:"));
2160
+ if (!ruleset || kept.length === rules.length) {
2161
+ return resourceOperation("cloudflare", "cache-rules", target.host, "missing", { zoneId });
2162
+ }
2163
+ cloudflareApiRequest(`/zones/${encodeURIComponent(zoneId)}/rulesets/${encodeURIComponent(ruleset.id)}`, {
2164
+ method: "PUT",
2165
+ body: { rules: kept },
2166
+ env
2167
+ });
2168
+ return resourceOperation("cloudflare", "cache-rules", target.host, "deleted", { zoneId, rulesetId: ruleset.id, removed: rules.length - kept.length });
2169
+ });
2170
+ }
2171
+ function configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope) {
2172
+ const normalizedScope = scope === "prod" ? "prod" : scope === "staging" ? "staging" : "local";
2173
+ if (normalizedScope === "local" || deployConfig.runtime?.mode !== "treeseed_managed") {
2174
+ return [];
2175
+ }
2176
+ let identity;
2177
+ try {
2178
+ identity = resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget(normalizedScope));
2179
+ } catch {
2180
+ identity = { deploymentKey: deployConfig.slug ?? deployConfig.name ?? "treeseed" };
2181
+ }
2182
+ const services = [];
2183
+ for (const serviceKey of ["api", "marketOperationsRunner"]) {
2184
+ const service = deployConfig.services?.[serviceKey];
2185
+ if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
2186
+ continue;
2187
+ }
2188
+ const baseServiceName = service.railway?.serviceName ?? `${identity.deploymentKey}-${serviceKey === "marketOperationsRunner" ? "market-operations-runner" : serviceKey}`;
2189
+ const runnerPool = serviceKey === "marketOperationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
2190
+ const count = serviceKey === "marketOperationsRunner" ? Math.max(1, Number.parseInt(String(runnerPool?.bootstrapCount ?? 1), 10) || 1) : 1;
2191
+ for (let index = 1; index <= count; index += 1) {
2192
+ const serviceName = serviceKey === "marketOperationsRunner" ? `${String(baseServiceName).replace(/-\d+$/u, "")}-${String(index).padStart(2, "0")}` : baseServiceName;
2193
+ services.push({
2194
+ key: serviceKey,
2195
+ projectName: service.railway?.projectName ?? identity.deploymentKey,
2196
+ serviceName,
2197
+ railwayEnvironment: normalizeRailwayEnvironmentName(service.environments?.[normalizedScope]?.railwayEnvironment ?? normalizedScope),
2198
+ domain: service.environments?.[normalizedScope]?.domain ?? null,
2199
+ volumeMountPath: serviceKey === "marketOperationsRunner" ? service.railway?.volumeMountPath ?? runnerPool?.volumeMountPath ?? "/data" : null
2200
+ });
2201
+ }
2202
+ }
2203
+ const marketDatabase = deployConfig.services?.marketDatabase;
2204
+ if (marketDatabase?.enabled !== false && marketDatabase?.provider === "railway" && marketDatabase?.railway?.resourceType === "postgres") {
2205
+ const baseName = typeof marketDatabase.railway?.serviceName === "string" && marketDatabase.railway.serviceName.trim() ? marketDatabase.railway.serviceName.trim() : `${deployConfig.slug ?? "treeseed-market"}-postgres`;
2206
+ services.push({
2207
+ key: "marketDatabase",
2208
+ projectName: deployConfig.services?.api?.railway?.projectName ?? identity.deploymentKey,
2209
+ serviceName: `${baseName.replace(/-(staging|prod|production)$/u, "")}-${normalizedScope === "prod" ? "prod" : normalizedScope}`,
2210
+ railwayEnvironment: normalizeRailwayEnvironmentName(normalizedScope),
2211
+ domain: null,
2212
+ volumeMountPath: null,
2213
+ dataStore: true
2214
+ });
2215
+ }
2216
+ return services;
2217
+ }
2218
+ async function destroyRailwayResources(tenantRoot, deployConfig, target, { dryRun = false, deleteData = false, env = process.env } = {}) {
2219
+ const scope = target.kind === "persistent" ? target.scope : target.branchName;
2220
+ const services = configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope);
2221
+ if (services.length === 0) {
2222
+ return { operations: [resourceOperation("railway", "environment", scope, "skipped", { reason: "not_applicable" })] };
2223
+ }
2224
+ const operations = [];
2225
+ if (!resolveRailwayApiToken(env)) {
2226
+ return {
2227
+ operations: services.map((service) => resourceOperation("railway", "service", service.serviceName, "blocked", { reason: "missing_railway_api_token" }))
2228
+ };
2229
+ }
2230
+ const workspace = await resolveRailwayWorkspaceContext({ env, workspace: resolveRailwayWorkspace(env) });
2231
+ const projects = await listRailwayProjects({ env, workspaceId: workspace.id });
2232
+ const projectNames = [...new Set(services.map((service) => service.projectName).filter(Boolean))];
2233
+ for (const projectName of projectNames) {
2234
+ const project = projects.find((entry) => !entry.deletedAt && (entry.name === projectName || entry.id === projectName)) ?? null;
2235
+ if (!project) {
2236
+ operations.push(resourceOperation("railway", "project", projectName, "missing"));
2237
+ continue;
2238
+ }
2239
+ const serviceTargets = services.filter((service) => service.projectName === projectName);
2240
+ const environmentName = normalizeRailwayEnvironmentName(scope);
2241
+ const environment = project.environments.find((entry) => entry.name === environmentName || entry.id === environmentName) ?? null;
2242
+ if (!environment) {
2243
+ operations.push(resourceOperation("railway", "environment", environmentName, "missing", { projectId: project.id }));
2244
+ }
2245
+ for (const service of serviceTargets) {
2246
+ const railwayService = project.services.find((entry) => entry.name === service.serviceName || entry.id === service.serviceName) ?? null;
2247
+ const shouldDeleteData = service.dataStore ? deleteData : true;
2248
+ operations.push(resourceOperation("railway", service.dataStore ? "postgres-service" : "service", service.serviceName, railwayService ? dryRun ? "planned" : "planned" : "missing", {
2249
+ projectId: project.id,
2250
+ serviceId: railwayService?.id ?? null,
2251
+ ...service.dataStore && !shouldDeleteData ? { status: "skipped", reason: "data_preserved" } : {}
2252
+ }));
2253
+ if (!railwayService || !environment) {
2254
+ continue;
2255
+ }
2256
+ if (shouldDeleteData) {
2257
+ const variables = await listRailwayVariables({
2258
+ projectId: project.id,
2259
+ environmentId: environment.id,
2260
+ serviceId: railwayService.id,
2261
+ env
2262
+ });
2263
+ for (const variableName of Object.keys(variables).sort()) {
2264
+ operations.push(resourceOperation("railway", "variable", `${service.serviceName}:${variableName}`, dryRun ? "planned" : "deleted", {
2265
+ projectId: project.id,
2266
+ serviceId: railwayService.id,
2267
+ environmentId: environment.id,
2268
+ reason: scope === "prod" && deleteData ? "project_delete" : "environment_delete"
2269
+ }));
2270
+ }
2271
+ }
2272
+ const instance = await getRailwayServiceInstance({ serviceId: railwayService.id, environmentId: environment.id, env });
2273
+ if (instance.cronSchedule) {
2274
+ operations.push(resourceOperation("railway", "schedule", `${service.serviceName}:${instance.cronSchedule}`, dryRun ? "planned" : "deleted", {
2275
+ projectId: project.id,
2276
+ serviceId: railwayService.id,
2277
+ environmentId: environment.id,
2278
+ reason: scope === "prod" && deleteData ? "project_delete" : "environment_delete"
2279
+ }));
2280
+ }
2281
+ if (!service.dataStore && service.domain) {
2282
+ const domains = await listRailwayCustomDomains({ projectId: project.id, environmentId: environment.id, serviceId: railwayService.id, env });
2283
+ for (const domain of domains.filter((entry) => entry.domain === service.domain)) {
2284
+ if (dryRun) {
2285
+ operations.push(resourceOperation("railway", "custom-domain", domain.domain, "planned", { id: domain.id }));
2286
+ } else {
2287
+ const result = await deleteRailwayCustomDomain({ domainId: domain.id, env });
2288
+ operations.push(resourceOperation("railway", "custom-domain", domain.domain, result.status, { id: domain.id }));
2289
+ }
2290
+ }
2291
+ }
2292
+ }
2293
+ const volumes = await listRailwayVolumes({ projectId: project.id, env });
2294
+ for (const volume of volumes) {
2295
+ const matchingInstance = volume.instances.find((instance) => instance.environmentId === environment?.id);
2296
+ if (!matchingInstance) {
2297
+ continue;
2298
+ }
2299
+ if (dryRun) {
2300
+ operations.push(resourceOperation("railway", "volume", volume.name, "planned", { id: volume.id, projectId: project.id }));
2301
+ } else {
2302
+ const result = await deleteRailwayVolume({ volumeId: volume.id, env });
2303
+ operations.push(resourceOperation("railway", "volume", volume.name, result.status, { id: volume.id, projectId: project.id }));
2304
+ }
2305
+ }
2306
+ const shouldDeleteProject = shouldDeleteRailwayProjectAfterEnvironmentDestroy(project, scope, deleteData, environment?.id ?? null);
2307
+ if (scope === "prod" && deleteData || shouldDeleteProject) {
2308
+ if (dryRun) {
2309
+ operations.push(resourceOperation("railway", "project", project.name, "planned", {
2310
+ id: project.id,
2311
+ reason: scope === "prod" ? "prod_delete_data_cleanup" : "no_managed_persistent_environments"
2312
+ }));
2313
+ } else {
2314
+ const result = await deleteRailwayProject({ projectId: project.id, env });
2315
+ operations.push(resourceOperation("railway", "project", project.name, result.status, {
2316
+ id: project.id,
2317
+ reason: scope === "prod" ? "prod_delete_data_cleanup" : "no_managed_persistent_environments"
2318
+ }));
2319
+ }
2320
+ } else if (environment) {
2321
+ if (dryRun) {
2322
+ operations.push(resourceOperation("railway", "environment", environment.name, "planned", { id: environment.id, projectId: project.id }));
2323
+ } else {
2324
+ const result = await deleteRailwayEnvironment({ environmentId: environment.id, env });
2325
+ operations.push(resourceOperation("railway", "environment", environment.name, result.status, { id: environment.id, projectId: project.id }));
2326
+ }
2327
+ }
2328
+ }
2329
+ return { operations };
2330
+ }
2331
+ function shouldDeleteRailwayProjectAfterEnvironmentDestroy(project, scope, deleteData, deletedEnvironmentId = null) {
2332
+ if (!deleteData || scope === "prod") {
2333
+ return false;
2334
+ }
2335
+ const managedPersistentNames = /* @__PURE__ */ new Set(["staging", "production", "prod"]);
2336
+ const targetEnvironmentName = normalizeRailwayEnvironmentName(scope);
2337
+ const remainingManagedEnvironments = (project?.environments ?? []).filter((environment) => environment?.id !== deletedEnvironmentId).filter((environment) => environment?.name !== targetEnvironmentName).filter((environment) => managedPersistentNames.has(environment?.name));
2338
+ return remainingManagedEnvironments.length === 0;
2339
+ }
2340
+ function killPidFromFile(filePath, { dryRun }) {
2341
+ const pid = Number.parseInt(readFileSync(filePath, "utf8").trim(), 10);
2342
+ if (!Number.isFinite(pid) || pid <= 0) {
2343
+ return resourceOperation("local", "dev-process", filePath, "missing");
2344
+ }
2345
+ if (dryRun) {
2346
+ return resourceOperation("local", "dev-process", String(pid), "planned", { pidFile: filePath });
2347
+ }
2348
+ try {
2349
+ process.kill(-pid, "SIGTERM");
2350
+ } catch {
2351
+ try {
2352
+ process.kill(pid, "SIGTERM");
2353
+ } catch {
2354
+ }
2355
+ }
2356
+ try {
2357
+ unlinkSync(filePath);
2358
+ } catch {
2359
+ }
2360
+ return resourceOperation("local", "dev-process", String(pid), "deleted", { pidFile: filePath });
2361
+ }
2362
+ function destroyLocalRuntimeResources(tenantRoot, { dryRun = false, deleteData = false } = {}) {
2363
+ const operations = [];
2364
+ const pidDir = resolve(tenantRoot, ".treeseed/dev-pids");
2365
+ if (existsSync(pidDir)) {
2366
+ for (const entry of readdirSync(pidDir)) {
2367
+ if (entry.endsWith(".pid")) {
2368
+ operations.push(killPidFromFile(resolve(pidDir, entry), { dryRun }));
2369
+ }
2370
+ }
2371
+ } else {
2372
+ operations.push(resourceOperation("local", "dev-pids", pidDir, "missing"));
2373
+ }
2374
+ if (deleteData) {
2375
+ for (const relativePath of [
2376
+ ".treeseed/generated/environments/local",
2377
+ ".treeseed/generated/dev",
2378
+ ".treeseed/market-operations-runner",
2379
+ ".treeseed/local-capacity-provider/data"
2380
+ ]) {
2381
+ const absolutePath = resolve(tenantRoot, relativePath);
2382
+ if (!existsSync(absolutePath)) {
2383
+ operations.push(resourceOperation("local", "data-path", relativePath, "missing"));
2384
+ continue;
2385
+ }
2386
+ if (dryRun) {
2387
+ operations.push(resourceOperation("local", "data-path", relativePath, "planned"));
2388
+ continue;
2389
+ }
2390
+ rmSync(absolutePath, { recursive: true, force: true });
2391
+ operations.push(resourceOperation("local", "data-path", relativePath, "deleted"));
2392
+ }
2393
+ } else {
2394
+ operations.push(resourceOperation("local", "data-path", ".treeseed/generated/environments/local", "skipped", { reason: "data_preserved" }));
2395
+ }
2396
+ return { operations };
2397
+ }
2398
+ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
1720
2399
  const target = normalizeTarget(options.scope ?? options.target ?? "prod");
1721
2400
  const deployConfig = loadTenantDeployConfig(tenantRoot);
1722
2401
  const state = loadDeployState(tenantRoot, deployConfig, { target });
@@ -1725,9 +2404,13 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
1725
2404
  CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
1726
2405
  };
1727
2406
  const dryRun = options.dryRun ?? false;
2407
+ const deleteData = options.deleteData === true;
1728
2408
  const force = options.force ?? false;
1729
2409
  const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
1730
2410
  const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
2411
+ const queues = listQueues(tenantRoot, env);
2412
+ const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2413
+ const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
1731
2414
  state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
1732
2415
  kvNamespaces,
1733
2416
  state.kvNamespaces.FORM_GUARD_KV.name,
@@ -1745,23 +2428,141 @@ function destroyCloudflareResources(tenantRoot, options = {}) {
1745
2428
  state.d1Databases.SITE_DATA_DB.databaseName,
1746
2429
  state.d1Databases.SITE_DATA_DB
1747
2430
  );
1748
- const worker = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
2431
+ const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
2432
+ const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
2433
+ const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
2434
+ const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2435
+ const workerResult = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
1749
2436
  const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
1750
2437
  const formGuardPreview = state.kvNamespaces.FORM_GUARD_KV.previewId && state.kvNamespaces.FORM_GUARD_KV.previewId !== state.kvNamespaces.FORM_GUARD_KV.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.previewId, { env, dryRun, preview: true }) : null;
1751
2438
  const session = state.kvNamespaces.SESSION?.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.id, { env, dryRun }) : null;
1752
2439
  const sessionPreview = state.kvNamespaces.SESSION?.previewId && state.kvNamespaces.SESSION.previewId !== state.kvNamespaces.SESSION.id ? deleteKvNamespace(tenantRoot, state.kvNamespaces.SESSION.previewId, { env, dryRun, preview: true }) : null;
1753
- const database = deleteD1Database(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun });
2440
+ const knownKvIds = new Set([
2441
+ state.kvNamespaces.FORM_GUARD_KV.id,
2442
+ state.kvNamespaces.FORM_GUARD_KV.previewId,
2443
+ state.kvNamespaces.SESSION?.id,
2444
+ state.kvNamespaces.SESSION?.previewId
2445
+ ].filter(Boolean));
2446
+ const legacyKvPrefix = state.identity?.deploymentKey ?? state.pages?.projectName ?? "";
2447
+ const legacyKvNamespaces = dryRun ? [] : kvNamespaces.filter((namespace) => {
2448
+ const title = typeof namespace?.title === "string" ? namespace.title : "";
2449
+ const id = typeof namespace?.id === "string" ? namespace.id : "";
2450
+ return title && id && !knownKvIds.has(id) && legacyKvPrefix && title.includes(legacyKvPrefix) && title.includes(target.scope);
2451
+ }).map((namespace) => {
2452
+ const result = deleteKvNamespace(tenantRoot, namespace.id, { env, dryRun: false });
2453
+ return resourceOperation("cloudflare", "kv-namespace", namespace.title, result.status, { ...result, legacy: true });
2454
+ });
2455
+ const database = deleteD1DatabaseForDestroy(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun, deleteData });
2456
+ const deletedQueue = deleteQueueByName(tenantRoot, queue ?? { name: state.queues?.agentWork?.name }, { env, dryRun });
2457
+ const deletedDlq = state.queues?.agentWork?.dlqName ? deleteQueueByName(tenantRoot, dlq ?? { name: state.queues.agentWork.dlqName }, { env, dryRun }) : null;
2458
+ const knownQueueNames = new Set([
2459
+ state.queues?.agentWork?.name,
2460
+ state.queues?.agentWork?.dlqName
2461
+ ].filter(Boolean));
2462
+ const legacyQueues = legacyQueueDestroyOperations(tenantRoot, queues, target, knownQueueNames, { env, dryRun });
2463
+ const r2Bucket = bucket || dryRun ? deleteR2Bucket(tenantRoot, state.content?.bucketName, { env, dryRun, deleteData }) : resourceOperation("cloudflare", "r2-bucket", state.content?.bucketName, "missing");
2464
+ const pageDnsNames = [
2465
+ state.pages?.customDomain,
2466
+ deployConfig.surfaces?.web?.environments?.[target.scope]?.domain,
2467
+ target.scope === "prod" ? primaryHost(deployConfig.surfaces?.web?.publicBaseUrl ?? deployConfig.siteUrl) : null
2468
+ ].filter(Boolean);
2469
+ const apiDnsNames = [
2470
+ deployConfig.services?.api?.environments?.[target.scope]?.domain,
2471
+ deployConfig.surfaces?.api?.environments?.[target.scope]?.domain
2472
+ ].filter(Boolean);
2473
+ const dnsRecords = [.../* @__PURE__ */ new Set([...pageDnsNames, ...apiDnsNames])].flatMap((name) => deleteDnsRecordsForName(deployConfig, name, { env, dryRun }));
2474
+ const cacheRules = deleteTreeseedCacheRules(deployConfig, state, { env, dryRun });
2475
+ const pageCustomDomains = pagesProject || dryRun ? deletePagesCustomDomains(tenantRoot, state.pages?.projectName, pageDnsNames, { env, dryRun }) : [resourceOperation("cloudflare", "pages-custom-domain", state.pages?.projectName, "missing")];
2476
+ const pageDeployments = pagesProject || dryRun ? deletePagesDeployments(tenantRoot, state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-deployments", state.pages?.projectName, "missing");
2477
+ const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
2478
+ const local = target.kind === "persistent" && target.scope === "local" ? destroyLocalRuntimeResources(tenantRoot, { dryRun, deleteData }) : { operations: [] };
2479
+ const railway = await destroyRailwayResources(tenantRoot, deployConfig, target, { dryRun, deleteData, env: process.env });
2480
+ const operations = {
2481
+ cloudflare: [
2482
+ resourceOperation("cloudflare", "worker", state.workerName, workerResult.status, workerResult),
2483
+ resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.FORM_GUARD_KV.name, formGuard.status, formGuard),
2484
+ ...formGuardPreview ? [resourceOperation("cloudflare", "kv-namespace-preview", state.kvNamespaces.FORM_GUARD_KV.name, formGuardPreview.status, formGuardPreview)] : [],
2485
+ ...session ? [resourceOperation("cloudflare", "kv-namespace", state.kvNamespaces.SESSION.name, session.status, session)] : [],
2486
+ ...sessionPreview ? [resourceOperation("cloudflare", "kv-namespace-preview", state.kvNamespaces.SESSION.name, sessionPreview.status, sessionPreview)] : [],
2487
+ ...legacyKvNamespaces,
2488
+ database,
2489
+ deletedQueue,
2490
+ ...deletedDlq ? [deletedDlq] : [],
2491
+ ...legacyQueues,
2492
+ r2Bucket,
2493
+ ...pageCustomDomains,
2494
+ pageDeployments,
2495
+ pages,
2496
+ ...dnsRecords,
2497
+ ...cacheRules
2498
+ ],
2499
+ railway: railway.operations,
2500
+ local: local.operations
2501
+ };
1754
2502
  return {
1755
2503
  target,
2504
+ deleteData,
1756
2505
  summary: buildDestroySummary(deployConfig, state, target),
1757
- operations: {
1758
- worker,
1759
- formGuard,
1760
- formGuardPreview,
1761
- session,
1762
- sessionPreview,
1763
- database
1764
- }
2506
+ operations
2507
+ };
2508
+ }
2509
+ function destroyCloudflareResources(tenantRoot, options = {}) {
2510
+ const target = normalizeTarget(options.scope ?? options.target ?? "prod");
2511
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
2512
+ const state = loadDeployState(tenantRoot, deployConfig, { target });
2513
+ state.workerName = targetWorkerName(deployConfig, target);
2514
+ const env = {
2515
+ CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
2516
+ };
2517
+ const dryRun = options.dryRun ?? false;
2518
+ const deleteData = options.deleteData === true;
2519
+ const force = options.force ?? false;
2520
+ const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
2521
+ const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
2522
+ const queues = listQueues(tenantRoot, env);
2523
+ const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
2524
+ const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
2525
+ state.kvNamespaces.FORM_GUARD_KV.id = resolveExistingKvIdByName(
2526
+ kvNamespaces,
2527
+ state.kvNamespaces.FORM_GUARD_KV.name,
2528
+ state.kvNamespaces.FORM_GUARD_KV.id
2529
+ );
2530
+ state.d1Databases.SITE_DATA_DB = resolveExistingD1ByName(
2531
+ d1Databases,
2532
+ state.d1Databases.SITE_DATA_DB.databaseName,
2533
+ state.d1Databases.SITE_DATA_DB
2534
+ );
2535
+ const queue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
2536
+ const dlq = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.dlqName);
2537
+ const bucket = buckets.find((entry) => entry?.name === state.content?.bucketName);
2538
+ const pagesProject = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
2539
+ const worker = deleteWorker(tenantRoot, state.workerName, { env, dryRun, force });
2540
+ const formGuard = deleteKvNamespace(tenantRoot, state.kvNamespaces.FORM_GUARD_KV.id, { env, dryRun });
2541
+ const database = deleteD1DatabaseForDestroy(tenantRoot, state.d1Databases.SITE_DATA_DB.databaseName, { env, dryRun, deleteData });
2542
+ const deletedQueue = deleteQueueByName(tenantRoot, queue ?? { name: state.queues?.agentWork?.name }, { env, dryRun });
2543
+ const deletedDlq = state.queues?.agentWork?.dlqName ? deleteQueueByName(tenantRoot, dlq ?? { name: state.queues.agentWork.dlqName }, { env, dryRun }) : null;
2544
+ const knownQueueNames = new Set([
2545
+ state.queues?.agentWork?.name,
2546
+ state.queues?.agentWork?.dlqName
2547
+ ].filter(Boolean));
2548
+ const legacyQueues = legacyQueueDestroyOperations(tenantRoot, queues, target, knownQueueNames, { env, dryRun });
2549
+ const r2Bucket = bucket || dryRun ? deleteR2Bucket(tenantRoot, state.content?.bucketName, { env, dryRun, deleteData }) : resourceOperation("cloudflare", "r2-bucket", state.content?.bucketName, "missing");
2550
+ const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
2551
+ const operations = {
2552
+ worker,
2553
+ formGuard,
2554
+ database,
2555
+ queue: deletedQueue,
2556
+ dlq: deletedDlq,
2557
+ legacyQueues,
2558
+ r2Bucket,
2559
+ pages
2560
+ };
2561
+ return {
2562
+ target,
2563
+ deleteData,
2564
+ summary: buildDestroySummary(deployConfig, state, target),
2565
+ operations
1765
2566
  };
1766
2567
  }
1767
2568
  function cleanupDestroyedState(tenantRoot, options = {}) {
@@ -1770,6 +2571,9 @@ function cleanupDestroyedState(tenantRoot, options = {}) {
1770
2571
  const { statePath, generatedRoot } = resolveTargetPaths(tenantRoot, target);
1771
2572
  rmSync(statePath, { force: true });
1772
2573
  rmSync(generatedRoot, { recursive: true, force: true });
2574
+ if (options.removeBuildArtifacts) {
2575
+ rmSync(resolve(tenantRoot, "dist"), { recursive: true, force: true });
2576
+ }
1773
2577
  return;
1774
2578
  }
1775
2579
  rmSync(resolve(tenantRoot, STATE_ROOT), { recursive: true, force: true });
@@ -2268,21 +3072,39 @@ function printDeploySummary(summary) {
2268
3072
  }
2269
3073
  function printDestroySummary(result) {
2270
3074
  const { summary, operations } = result;
3075
+ const cloudflare = Array.isArray(operations?.cloudflare) ? operations.cloudflare : null;
3076
+ const legacy = cloudflare ? {
3077
+ worker: cloudflare.find((entry) => entry.type === "worker"),
3078
+ database: cloudflare.find((entry) => entry.type === "d1-database"),
3079
+ formGuard: cloudflare.find((entry) => entry.type === "kv-namespace"),
3080
+ formGuardPreview: cloudflare.find((entry) => entry.type === "kv-namespace-preview"),
3081
+ session: cloudflare.find((entry) => entry.type === "kv-namespace" && entry.name === summary.sessionKv?.name),
3082
+ sessionPreview: cloudflare.find((entry) => entry.type === "kv-namespace-preview" && entry.name === summary.sessionKv?.name)
3083
+ } : operations;
2271
3084
  console.log("Treeseed destroy summary");
2272
3085
  console.log(` Target: ${summary.target}`);
2273
- console.log(` Worker: ${summary.workerName} -> ${operations.worker.status}`);
3086
+ console.log(` Worker: ${summary.workerName} -> ${legacy.worker?.status ?? "unknown"}`);
2274
3087
  console.log(` Site URL: ${summary.siteUrl}`);
2275
3088
  console.log(` Account ID: ${summary.accountId}`);
2276
- console.log(` D1: ${summary.siteDataDb.databaseName} -> ${operations.database.status}`);
2277
- console.log(` KV FORM_GUARD_KV: ${summary.formGuardKv.name} -> ${operations.formGuard.status}`);
2278
- if (operations.formGuardPreview) {
2279
- console.log(` KV FORM_GUARD_KV preview -> ${operations.formGuardPreview.status}`);
2280
- }
2281
- if (summary.sessionKv && operations.session) {
2282
- console.log(` KV SESSION (deprecated): ${summary.sessionKv.name} -> ${operations.session.status}`);
2283
- }
2284
- if (operations.sessionPreview) {
2285
- console.log(` KV SESSION preview -> ${operations.sessionPreview.status}`);
3089
+ console.log(` D1: ${summary.siteDataDb.databaseName} -> ${legacy.database?.status ?? "unknown"}`);
3090
+ console.log(` KV FORM_GUARD_KV: ${summary.formGuardKv.name} -> ${legacy.formGuard?.status ?? "unknown"}`);
3091
+ if (legacy.formGuardPreview) {
3092
+ console.log(` KV FORM_GUARD_KV preview -> ${legacy.formGuardPreview.status}`);
3093
+ }
3094
+ if (summary.sessionKv && legacy.session) {
3095
+ console.log(` KV SESSION (deprecated): ${summary.sessionKv.name} -> ${legacy.session.status}`);
3096
+ }
3097
+ if (legacy.sessionPreview) {
3098
+ console.log(` KV SESSION preview -> ${legacy.sessionPreview.status}`);
3099
+ }
3100
+ if (cloudflare) {
3101
+ for (const entry of [
3102
+ ...cloudflare.filter((item) => !["worker", "d1-database", "kv-namespace", "kv-namespace-preview"].includes(item.type)),
3103
+ ...Array.isArray(operations?.railway) ? operations.railway : [],
3104
+ ...Array.isArray(operations?.local) ? operations.local : []
3105
+ ]) {
3106
+ console.log(` ${entry.provider} ${entry.type} ${entry.name ?? "(none)"} -> ${entry.status}`);
3107
+ }
2286
3108
  }
2287
3109
  }
2288
3110
  export {
@@ -2300,6 +3122,7 @@ export {
2300
3122
  deployTargetLabel,
2301
3123
  deriveTreeseedStagingSurfaceDomain,
2302
3124
  destroyCloudflareResources,
3125
+ destroyTreeseedEnvironmentResources,
2303
3126
  ensureGeneratedWranglerConfig,
2304
3127
  finalizeDeploymentState,
2305
3128
  hasProvisionedCloudflareResources,
@@ -2333,6 +3156,7 @@ export {
2333
3156
  runRemoteD1Migrations,
2334
3157
  runWrangler,
2335
3158
  scopeFromTarget,
3159
+ shouldDeleteRailwayProjectAfterEnvironmentDestroy,
2336
3160
  syncCloudflareSecrets,
2337
3161
  validateDeployPrerequisites,
2338
3162
  validateDestroyPrerequisites,