@treeseed/sdk 0.10.28 → 0.11.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.
Files changed (148) hide show
  1. package/README.md +207 -6
  2. package/dist/capacity-provider.d.ts +3 -1
  3. package/dist/capacity-provider.js +25 -5
  4. package/dist/control-plane.d.ts +1 -0
  5. package/dist/control-plane.js +38 -13
  6. package/dist/db/market-schema.d.ts +8860 -6172
  7. package/dist/db/market-schema.js +108 -0
  8. package/dist/db/node-sqlite.js +7 -2
  9. package/dist/hosting/apps.d.ts +12 -0
  10. package/dist/hosting/apps.js +107 -0
  11. package/dist/hosting/builtins.d.ts +25 -0
  12. package/dist/hosting/builtins.js +791 -0
  13. package/dist/hosting/contracts.d.ts +207 -0
  14. package/dist/hosting/contracts.js +0 -0
  15. package/dist/hosting/graph.d.ts +192 -0
  16. package/dist/hosting/graph.js +1106 -0
  17. package/dist/hosting/index.d.ts +4 -0
  18. package/dist/hosting/index.js +4 -0
  19. package/dist/index.d.ts +10 -3
  20. package/dist/index.js +63 -6
  21. package/dist/managed-dependencies.js +1 -2
  22. package/dist/market-client.d.ts +63 -3
  23. package/dist/market-client.js +83 -11
  24. package/dist/operations/services/bootstrap-runner.d.ts +3 -1
  25. package/dist/operations/services/bootstrap-runner.js +22 -2
  26. package/dist/operations/services/config-runtime.d.ts +10 -5
  27. package/dist/operations/services/config-runtime.js +209 -66
  28. package/dist/operations/services/deploy.d.ts +70 -7
  29. package/dist/operations/services/deploy.js +579 -64
  30. package/dist/operations/services/deployment-readiness.d.ts +30 -0
  31. package/dist/operations/services/deployment-readiness.js +175 -0
  32. package/dist/operations/services/git-workflow.d.ts +2 -1
  33. package/dist/operations/services/git-workflow.js +9 -3
  34. package/dist/operations/services/github-actions-verification.d.ts +1 -0
  35. package/dist/operations/services/github-actions-verification.js +1 -0
  36. package/dist/operations/services/github-api.js +1 -1
  37. package/dist/operations/services/github-automation.d.ts +1 -1
  38. package/dist/operations/services/github-automation.js +4 -3
  39. package/dist/operations/services/github-credentials.d.ts +13 -0
  40. package/dist/operations/services/github-credentials.js +58 -0
  41. package/dist/operations/services/hosted-service-checks.d.ts +63 -0
  42. package/dist/operations/services/hosted-service-checks.js +327 -0
  43. package/dist/operations/services/hub-provider-launch.js +3 -3
  44. package/dist/operations/services/live-hosted-service-checks.d.ts +25 -0
  45. package/dist/operations/services/live-hosted-service-checks.js +350 -0
  46. package/dist/operations/services/managed-host-security.js +1 -1
  47. package/dist/operations/services/operations-runner-smoke.d.ts +30 -0
  48. package/dist/operations/services/operations-runner-smoke.js +180 -0
  49. package/dist/operations/services/package-adapters.d.ts +95 -0
  50. package/dist/operations/services/package-adapters.js +288 -0
  51. package/dist/operations/services/package-reference-policy.d.ts +1 -0
  52. package/dist/operations/services/package-reference-policy.js +15 -2
  53. package/dist/operations/services/project-platform.d.ts +80 -22
  54. package/dist/operations/services/project-platform.js +49 -8
  55. package/dist/operations/services/project-web-monitor.js +26 -4
  56. package/dist/operations/services/railway-api.d.ts +88 -5
  57. package/dist/operations/services/railway-api.js +626 -35
  58. package/dist/operations/services/railway-deploy.d.ts +46 -40
  59. package/dist/operations/services/railway-deploy.js +261 -293
  60. package/dist/operations/services/release-candidate.d.ts +19 -0
  61. package/dist/operations/services/release-candidate.js +375 -38
  62. package/dist/operations/services/repository-save-orchestrator.d.ts +3 -1
  63. package/dist/operations/services/repository-save-orchestrator.js +279 -66
  64. package/dist/operations/services/runtime-tools.d.ts +1 -0
  65. package/dist/operations/services/runtime-tools.js +10 -9
  66. package/dist/operations/services/verification-cache.d.ts +25 -0
  67. package/dist/operations/services/verification-cache.js +71 -0
  68. package/dist/operations/services/workspace-dependency-mode.js +9 -1
  69. package/dist/operations/services/workspace-save.js +1 -1
  70. package/dist/operations/services/workspace-tools.js +2 -1
  71. package/dist/platform/contracts.d.ts +32 -1
  72. package/dist/platform/deploy-config.js +73 -8
  73. package/dist/platform/env.yaml +163 -35
  74. package/dist/platform/environment.d.ts +1 -0
  75. package/dist/platform/environment.js +74 -5
  76. package/dist/platform/plugin.d.ts +9 -0
  77. package/dist/platform-operation-store.js +2 -2
  78. package/dist/platform-operations.js +1 -1
  79. package/dist/reconcile/bootstrap-systems.js +2 -2
  80. package/dist/reconcile/builtin-adapters.js +372 -189
  81. package/dist/reconcile/contracts.d.ts +9 -5
  82. package/dist/reconcile/desired-state.d.ts +1 -0
  83. package/dist/reconcile/desired-state.js +5 -5
  84. package/dist/reconcile/engine.d.ts +5 -2
  85. package/dist/reconcile/engine.js +53 -32
  86. package/dist/reconcile/index.d.ts +2 -0
  87. package/dist/reconcile/index.js +2 -0
  88. package/dist/reconcile/live-acceptance.d.ts +79 -0
  89. package/dist/reconcile/live-acceptance.js +1615 -0
  90. package/dist/reconcile/platform.d.ts +104 -0
  91. package/dist/reconcile/platform.js +100 -0
  92. package/dist/reconcile/state.js +4 -4
  93. package/dist/reconcile/units.js +2 -2
  94. package/dist/scripts/deployment-readiness.js +20 -0
  95. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  96. package/dist/scripts/operations-runner-smoke.js +16 -0
  97. package/dist/scripts/release-verify.js +4 -1
  98. package/dist/scripts/tenant-workflow-action.js +10 -1
  99. package/dist/sdk-types.d.ts +169 -4
  100. package/dist/sdk-types.js +20 -2
  101. package/dist/sdk.d.ts +35 -24
  102. package/dist/sdk.js +186 -17
  103. package/dist/template-launch-requirements.js +9 -0
  104. package/dist/treedx/adapters.d.ts +6 -0
  105. package/dist/treedx/adapters.js +36 -0
  106. package/dist/treedx/client.d.ts +222 -0
  107. package/dist/treedx/client.js +871 -0
  108. package/dist/treedx/errors.d.ts +13 -0
  109. package/dist/treedx/errors.js +17 -0
  110. package/dist/treedx/federated-client.d.ts +27 -0
  111. package/dist/treedx/federated-client.js +158 -0
  112. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  113. package/dist/treedx/generated/openapi-types.js +0 -0
  114. package/dist/treedx/graph-adapter.d.ts +33 -0
  115. package/dist/treedx/graph-adapter.js +156 -0
  116. package/dist/treedx/index.d.ts +14 -0
  117. package/dist/treedx/index.js +48 -0
  118. package/dist/treedx/market-integration.d.ts +27 -0
  119. package/dist/treedx/market-integration.js +131 -0
  120. package/dist/treedx/ports.d.ts +166 -0
  121. package/dist/treedx/ports.js +231 -0
  122. package/dist/treedx/query-adapter.d.ts +19 -0
  123. package/dist/treedx/query-adapter.js +62 -0
  124. package/dist/treedx/registry-client.d.ts +11 -0
  125. package/dist/treedx/registry-client.js +19 -0
  126. package/dist/treedx/repository-adapter.d.ts +45 -0
  127. package/dist/treedx/repository-adapter.js +308 -0
  128. package/dist/treedx/sdk-integration.d.ts +27 -0
  129. package/dist/treedx/sdk-integration.js +63 -0
  130. package/dist/treedx/types.d.ts +1084 -0
  131. package/dist/treedx/types.js +8 -0
  132. package/dist/treedx/workspace-adapter.d.ts +27 -0
  133. package/dist/treedx/workspace-adapter.js +65 -0
  134. package/dist/treedx-backends.d.ts +218 -0
  135. package/dist/treedx-backends.js +632 -0
  136. package/dist/treedx-client.d.ts +86 -0
  137. package/dist/treedx-client.js +175 -0
  138. package/dist/treeseed/template-catalog/catalog.fixture.json +23 -23
  139. package/dist/workflow/operations.d.ts +119 -13
  140. package/dist/workflow/operations.js +309 -53
  141. package/dist/workflow-state.d.ts +13 -0
  142. package/dist/workflow-state.js +43 -26
  143. package/dist/workflow-support.d.ts +11 -3
  144. package/dist/workflow-support.js +67 -3
  145. package/dist/workflow.d.ts +5 -0
  146. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  147. package/package.json +34 -3
  148. package/templates/github/deploy-web.workflow.yml +39 -6
@@ -319,7 +319,7 @@ function buildPublicVars(deployConfig, options = {}) {
319
319
  TREESEED_RUNTIME_MODE: deployConfig.runtime?.mode ?? "none",
320
320
  TREESEED_RUNTIME_REGISTRATION: deployConfig.runtime?.registration ?? "none",
321
321
  TREESEED_CENTRAL_MARKET_API_BASE_URL: envOrNull("TREESEED_CENTRAL_MARKET_API_BASE_URL") ?? DEFAULT_TREESEED_MARKET_BASE_URL,
322
- TREESEED_MARKET_API_BASE_URL: resolveConfiguredMarketBaseUrl(deployConfig),
322
+ TREESEED_API_BASE_URL: resolveConfiguredMarketBaseUrl(deployConfig),
323
323
  TREESEED_CATALOG_MARKET_API_BASE_URLS: envOrNull("TREESEED_CATALOG_MARKET_API_BASE_URLS") ?? resolveConfiguredMarketBaseUrl(deployConfig),
324
324
  TREESEED_HOSTING_TEAM_ID: contentDefaultTeamId,
325
325
  TREESEED_PROJECT_ID: identity.projectId,
@@ -906,7 +906,7 @@ function runWrangler(args, { cwd, allowFailure = false, json = false, capture =
906
906
  output || `Wrangler command failed: ${args.join(" ")}`,
907
907
  "",
908
908
  "Treeseed Cloudflare authentication failed. Check that CLOUDFLARE_API_TOKEN is an account-level token scoped to the target account and domain.",
909
- "Required Cloudflare permissions: Account Cloudflare Pages edit, Account Workers Scripts edit, Account Workers KV Storage edit, Account D1 edit, Account Queues edit, Zone DNS edit."
909
+ "Required Cloudflare permissions: account Pages Write, Workers Scripts Write, Workers KV Storage Write, Workers R2 Storage Write, D1 Write, Queues Write, Turnstile Sites Write, Account Rulesets Write, and Account Rule Lists Write; target zone Zone Read, DNS Write, Cache Settings Write, and SSL and Certificates Write."
910
910
  ].join("\n"));
911
911
  }
912
912
  throw new Error(output || `Wrangler command failed: ${args.join(" ")}`);
@@ -990,6 +990,48 @@ function listTurnstileWidgets(tenantRoot, env) {
990
990
  });
991
991
  return Array.isArray(payload?.result) ? payload.result : [];
992
992
  }
993
+ function listWorkers(tenantRoot, env) {
994
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
995
+ if (!accountId) {
996
+ return [];
997
+ }
998
+ const payload = cloudflareApiRequest(`/accounts/${encodeURIComponent(accountId)}/workers/services?per_page=100`, {
999
+ env,
1000
+ allowFailure: true
1001
+ });
1002
+ return Array.isArray(payload?.result) ? payload.result : [];
1003
+ }
1004
+ function listDnsZones(env) {
1005
+ const payload = cloudflareApiRequest("/zones?per_page=100", {
1006
+ env,
1007
+ allowFailure: true
1008
+ });
1009
+ return Array.isArray(payload?.result) ? payload.result : [];
1010
+ }
1011
+ function listDnsRecords(zoneId, env) {
1012
+ if (!zoneId) {
1013
+ return [];
1014
+ }
1015
+ const records = [];
1016
+ let page = 1;
1017
+ let totalPages = 1;
1018
+ while (page <= totalPages && page <= 50) {
1019
+ const payload = cloudflareApiRequest(
1020
+ `/zones/${encodeURIComponent(zoneId)}/dns_records?per_page=100&page=${page}`,
1021
+ { env, allowFailure: true }
1022
+ );
1023
+ if (payload?.success === false) {
1024
+ break;
1025
+ }
1026
+ if (Array.isArray(payload?.result)) {
1027
+ records.push(...payload.result);
1028
+ }
1029
+ const reportedTotal = Number(payload?.result_info?.total_pages);
1030
+ totalPages = Number.isFinite(reportedTotal) && reportedTotal > 0 ? reportedTotal : page;
1031
+ page += 1;
1032
+ }
1033
+ return records;
1034
+ }
993
1035
  function getTurnstileWidget(env, sitekey) {
994
1036
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
995
1037
  if (!accountId || !sitekey) {
@@ -1199,6 +1241,13 @@ function shouldManageCloudflareWebCacheRules(deployConfig, target) {
1199
1241
  return Boolean(webTarget?.host && !webTarget.host.endsWith(".workers.dev") && !webTarget.host.endsWith(".pages.dev"));
1200
1242
  }
1201
1243
  function cloudflareApiRequest(path, { method = "GET", body, env, allowFailure = false } = {}) {
1244
+ const token = env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? "";
1245
+ if (!token) {
1246
+ if (allowFailure) {
1247
+ return null;
1248
+ }
1249
+ throw new Error(`Cloudflare API token is required: ${method} ${path}`);
1250
+ }
1202
1251
  const requestScript = `import { readFileSync } from 'node:fs';
1203
1252
  import { request } from 'node:https';
1204
1253
  const input = JSON.parse(readFileSync(0, 'utf8') || '{}');
@@ -1262,9 +1311,13 @@ try {
1262
1311
  method,
1263
1312
  body,
1264
1313
  timeoutMs: 12e3,
1265
- token: env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? ""
1314
+ token
1266
1315
  });
1267
- const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted/iu.test(text || "");
1316
+ const isTransient = (text) => /fetch failed|timed out|etimedout|econnreset|enetunreach|temporarily unavailable|aborted|rate limit|too many requests|throttl|please wait/iu.test(text || "");
1317
+ const retryDelay = (text, currentAttempt) => {
1318
+ const base = /rate limit|too many requests|throttl|please wait/iu.test(text || "") ? 2500 : 500;
1319
+ return base * (currentAttempt + 1);
1320
+ };
1268
1321
  const formatPayloadErrors = (payload) => Array.isArray(payload?.errors) ? payload.errors.map((entry) => entry?.message ?? JSON.stringify(entry)).join("; ") : "";
1269
1322
  const summarizeChildError = (text) => {
1270
1323
  const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
@@ -1288,9 +1341,9 @@ try {
1288
1341
  }
1289
1342
  );
1290
1343
  if (response.error?.code === "ETIMEDOUT") {
1291
- if (attempt < 4) {
1344
+ if (attempt < 7) {
1292
1345
  attempt += 1;
1293
- sleepSync(500 * attempt);
1346
+ sleepSync(retryDelay("timed out", attempt));
1294
1347
  continue;
1295
1348
  }
1296
1349
  if (!allowFailure) {
@@ -1300,9 +1353,9 @@ try {
1300
1353
  }
1301
1354
  const stderr = response.stderr?.trim() || "";
1302
1355
  if (response.status !== 0) {
1303
- if (attempt < 4 && isTransient(stderr)) {
1356
+ if (attempt < 7 && isTransient(stderr)) {
1304
1357
  attempt += 1;
1305
- sleepSync(500 * attempt);
1358
+ sleepSync(retryDelay(stderr, attempt));
1306
1359
  continue;
1307
1360
  }
1308
1361
  if (!allowFailure) {
@@ -1323,9 +1376,9 @@ try {
1323
1376
  };
1324
1377
  }
1325
1378
  const details = formatPayloadErrors(parsed.payload);
1326
- if (!parsed.ok && parsed.transient && attempt < 4 && isTransient(details)) {
1379
+ if (!parsed.ok && isTransient(details) && attempt < 7) {
1327
1380
  attempt += 1;
1328
- sleepSync(500 * attempt);
1381
+ sleepSync(retryDelay(details, attempt));
1329
1382
  continue;
1330
1383
  }
1331
1384
  if (!parsed.ok && !allowFailure) {
@@ -1635,7 +1688,7 @@ function resolveConfiguredCloudflareAccountId(deployConfig) {
1635
1688
  return envOrNull("CLOUDFLARE_ACCOUNT_ID") ?? deployConfig.cloudflare.accountId;
1636
1689
  }
1637
1690
  function resolveConfiguredMarketBaseUrl(deployConfig) {
1638
- return envOrNull("TREESEED_MARKET_API_BASE_URL") ?? envOrNull("TREESEED_CENTRAL_MARKET_API_BASE_URL") ?? deployConfig.runtime?.marketBaseUrl ?? deployConfig.hosting?.marketBaseUrl ?? DEFAULT_TREESEED_MARKET_BASE_URL;
1691
+ return envOrNull("TREESEED_API_BASE_URL") ?? envOrNull("TREESEED_CENTRAL_MARKET_API_BASE_URL") ?? deployConfig.runtime?.marketBaseUrl ?? deployConfig.hosting?.marketBaseUrl ?? DEFAULT_TREESEED_MARKET_BASE_URL;
1639
1692
  }
1640
1693
  function resolveConfiguredPagesProjectName(deployConfig) {
1641
1694
  return sharedDeploymentName(resolveTreeseedResourceIdentity(deployConfig, createPersistentDeployTarget("prod")));
@@ -1803,7 +1856,7 @@ function resolveExistingD1ByName(d1Databases, expectedName, current) {
1803
1856
  };
1804
1857
  }
1805
1858
  function looksLikeMissingResource(output) {
1806
- return /not found|does not exist|could(?: not|n't) find|couldnt find/i.test(output);
1859
+ return /not found|does not exist|could(?: not|n't) find|couldnt find|already deleted|deleted widget|access a deleted/i.test(output);
1807
1860
  }
1808
1861
  function deleteKvNamespace(tenantRoot, namespaceId, { env, dryRun, preview = false }) {
1809
1862
  if (!namespaceId || isPlaceholderResourceId(namespaceId)) {
@@ -1957,9 +2010,9 @@ function listR2Objects(bucketName, { env }) {
1957
2010
  }
1958
2011
  const objects = [];
1959
2012
  let cursor = "";
1960
- for (let page = 0; page < 100; page += 1) {
2013
+ for (let page = 0; page < 20; page += 1) {
1961
2014
  const payload = cloudflareApiRequest(
1962
- `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects?per_page=1000${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
2015
+ `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects?per_page=200${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
1963
2016
  { env, allowFailure: true }
1964
2017
  );
1965
2018
  if (payload?.success === false) {
@@ -1972,45 +2025,156 @@ function listR2Objects(bucketName, { env }) {
1972
2025
  break;
1973
2026
  }
1974
2027
  cursor = nextCursor;
2028
+ if (objects.length >= 200) {
2029
+ break;
2030
+ }
1975
2031
  }
1976
2032
  return objects;
1977
2033
  }
1978
2034
  function drainR2Bucket(bucketName, { env }) {
1979
2035
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
1980
2036
  if (!accountId || !bucketName) {
1981
- return { objectsDeleted: 0 };
2037
+ return { objectsDeleted: 0, objectsMissing: 0, objectsDeferred: 0 };
1982
2038
  }
1983
2039
  let objectsDeleted = 0;
2040
+ let objectsMissing = 0;
2041
+ let objectsDeferred = 0;
1984
2042
  for (let batch = 0; batch < 100; batch += 1) {
1985
2043
  const objects = listR2Objects(bucketName, { env });
1986
2044
  if (objects.length === 0) {
1987
2045
  break;
1988
2046
  }
1989
- let batchDeleted = 0;
1990
- for (const object of objects) {
1991
- const key = r2ObjectKey(object);
1992
- if (!key) {
2047
+ const keys = objects.map((object) => r2ObjectKey(object)).filter(Boolean);
2048
+ const deleted = deleteR2ObjectsBatch(bucketName, keys, { env });
2049
+ objectsDeleted += deleted.objectsDeleted;
2050
+ objectsMissing += deleted.objectsMissing;
2051
+ objectsDeferred += deleted.objectsDeferred;
2052
+ const batchDeleted = deleted.objectsDeleted + deleted.objectsMissing;
2053
+ if (batchDeleted === 0) {
2054
+ if (deleted.objectsDeferred > 0) {
2055
+ sleepSync(3e3);
1993
2056
  continue;
1994
2057
  }
1995
- const result = cloudflareApiRequest(
1996
- `/accounts/${encodeURIComponent(accountId)}/r2/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(key)}`,
1997
- { method: "DELETE", env, allowFailure: true }
1998
- );
1999
- if (result?.success === false) {
2000
- const message = formatCloudflareErrors(result);
2001
- if (looksLikeMissingResource(message)) {
2002
- continue;
2003
- }
2004
- throw new Error(message || `Failed to delete R2 object ${key}.`);
2005
- }
2006
- objectsDeleted += 1;
2007
- batchDeleted += 1;
2008
- }
2009
- if (batchDeleted === 0) {
2010
2058
  break;
2011
2059
  }
2060
+ if (deleted.objectsDeferred > 0) {
2061
+ sleepSync(1500);
2062
+ }
2063
+ }
2064
+ return { objectsDeleted, objectsMissing, objectsDeferred };
2065
+ }
2066
+ function deleteR2ObjectsBatch(bucketName, keys, { env }) {
2067
+ const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
2068
+ const token = env?.CLOUDFLARE_API_TOKEN ?? process.env.CLOUDFLARE_API_TOKEN ?? "";
2069
+ const uniqueKeys = [...new Set((keys ?? []).filter(Boolean))];
2070
+ if (!accountId || !bucketName || uniqueKeys.length === 0) {
2071
+ return { objectsDeleted: 0, objectsMissing: 0, objectsDeferred: 0 };
2072
+ }
2073
+ const script = `
2074
+ const input = JSON.parse(await new Promise((resolve) => {
2075
+ let body = '';
2076
+ process.stdin.setEncoding('utf8');
2077
+ process.stdin.on('data', (chunk) => { body += chunk; });
2078
+ process.stdin.on('end', () => resolve(body || '{}'));
2079
+ }));
2080
+ let index = 0;
2081
+ let deleted = 0;
2082
+ let missing = 0;
2083
+ let deferred = 0;
2084
+ const failed = [];
2085
+ async function removeKey(key) {
2086
+ function encodeObjectKey(value) {
2087
+ return String(value).split('/').map((part) => encodeURIComponent(part)).join('/');
2088
+ }
2089
+ const url = 'https://api.cloudflare.com/client/v4/accounts/'
2090
+ + encodeURIComponent(input.accountId)
2091
+ + '/r2/buckets/'
2092
+ + encodeURIComponent(input.bucketName)
2093
+ + '/objects/'
2094
+ + encodeObjectKey(key);
2095
+ for (let attempt = 0; attempt < 6; attempt += 1) {
2096
+ const controller = new AbortController();
2097
+ const timeout = setTimeout(() => controller.abort(), input.timeoutMs || 15000);
2098
+ try {
2099
+ const response = await fetch(url, {
2100
+ method: 'DELETE',
2101
+ headers: { authorization: 'Bearer ' + input.token },
2102
+ signal: controller.signal,
2103
+ });
2104
+ const text = await response.text();
2105
+ let payload = {};
2106
+ try { payload = text ? JSON.parse(text) : {}; } catch { payload = { errors: [{ message: text }] }; }
2107
+ if (response.ok && payload.success !== false) {
2108
+ deleted += 1;
2109
+ return;
2110
+ }
2111
+ const message = Array.isArray(payload.errors) ? payload.errors.map((entry) => entry?.message || JSON.stringify(entry)).join('; ') : text;
2112
+ if (/not found|does not exist|deleted|missing/i.test(message || '')) {
2113
+ missing += 1;
2114
+ return;
2115
+ }
2116
+ if (response.status === 429 || /rate limit|too many requests/i.test(message || '')) {
2117
+ await new Promise((resolve) => setTimeout(resolve, 750 * (attempt + 1)));
2118
+ continue;
2119
+ }
2120
+ failed.push({ key, message: message || \`delete failed with status \${response.status}\` });
2121
+ return;
2122
+ } catch (error) {
2123
+ if (attempt < 5 && /aborted|timed out|fetch failed|econnreset/i.test(error instanceof Error ? error.message : String(error))) {
2124
+ await new Promise((resolve) => setTimeout(resolve, 750 * (attempt + 1)));
2125
+ continue;
2126
+ }
2127
+ failed.push({ key, message: error instanceof Error ? error.message : String(error) });
2128
+ return;
2129
+ } finally {
2130
+ clearTimeout(timeout);
2131
+ }
2132
+ }
2133
+ deferred += 1;
2134
+ }
2135
+ async function worker() {
2136
+ for (;;) {
2137
+ const current = index;
2138
+ index += 1;
2139
+ if (current >= input.keys.length) return;
2140
+ await removeKey(input.keys[current]);
2141
+ }
2142
+ }
2143
+ await Promise.all(Array.from({ length: Math.min(input.concurrency || 4, input.keys.length) }, () => worker()));
2144
+ process.stdout.write(JSON.stringify({ deleted, missing, deferred, failed }));
2145
+ `.trim();
2146
+ const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
2147
+ stdio: ["pipe", "pipe", "pipe"],
2148
+ encoding: "utf8",
2149
+ env: { ...process.env, ...env ?? {} },
2150
+ input: JSON.stringify({
2151
+ accountId,
2152
+ bucketName,
2153
+ keys: uniqueKeys,
2154
+ token,
2155
+ concurrency: 4,
2156
+ timeoutMs: 12e3
2157
+ }),
2158
+ timeout: 12e4
2159
+ });
2160
+ if (result.status !== 0 || result.error) {
2161
+ throw new Error(result.stderr?.trim() || result.error?.message || `Failed to delete R2 object batch for ${bucketName}.`);
2012
2162
  }
2013
- return { objectsDeleted };
2163
+ let parsed;
2164
+ try {
2165
+ parsed = JSON.parse(result.stdout || "{}");
2166
+ } catch {
2167
+ throw new Error(`R2 object batch delete returned invalid JSON for ${bucketName}.`);
2168
+ }
2169
+ if (Array.isArray(parsed.failed) && parsed.failed.length > 0) {
2170
+ const first = parsed.failed[0];
2171
+ throw new Error(`Failed to delete ${parsed.failed.length} R2 objects from ${bucketName}: ${first?.message ?? first?.key ?? "unknown error"}`);
2172
+ }
2173
+ return {
2174
+ objectsDeleted: Number(parsed.deleted) || 0,
2175
+ objectsMissing: Number(parsed.missing) || 0,
2176
+ objectsDeferred: Number(parsed.deferred) || 0
2177
+ };
2014
2178
  }
2015
2179
  function deleteD1DatabaseForDestroy(tenantRoot, databaseName, { env, dryRun, deleteData }) {
2016
2180
  if (!deleteData) {
@@ -2066,20 +2230,20 @@ function listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env }) {
2066
2230
  return [];
2067
2231
  }
2068
2232
  }
2069
- function deletePagesCustomDomains(tenantRoot, projectName, knownNames, { env, dryRun }) {
2233
+ function deletePagesCustomDomains(tenantRoot, projectName, knownNames, { env, dryRun, knownOnly = false }) {
2070
2234
  if (!projectName) {
2071
2235
  return [resourceOperation("cloudflare", "pages-custom-domain", projectName, "missing")];
2072
2236
  }
2073
2237
  const desiredNames = [...new Set((knownNames ?? []).filter(Boolean))];
2074
2238
  if (dryRun) {
2075
- 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" })];
2239
+ return desiredNames.length > 0 ? desiredNames.map((name) => resourceOperation("cloudflare", "pages-custom-domain", name, "planned", { projectName, knownOnly })) : [resourceOperation("cloudflare", "pages-custom-domain", projectName, knownOnly ? "skipped" : "planned", { reason: knownOnly ? "no_target_scoped_domain" : "project_delete_prerequisite" })];
2076
2240
  }
2077
2241
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
2078
2242
  if (!accountId) {
2079
2243
  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" })];
2080
2244
  }
2081
- const listedNames = listPagesCustomDomains(projectName, { env }).map(pagesDomainName).filter(Boolean);
2082
- const wranglerNames = listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env });
2245
+ const listedNames = knownOnly ? [] : listPagesCustomDomains(projectName, { env }).map(pagesDomainName).filter(Boolean);
2246
+ const wranglerNames = knownOnly ? [] : listPagesCustomDomainsWithWrangler(tenantRoot, projectName, { env });
2083
2247
  const domainNames = [.../* @__PURE__ */ new Set([...desiredNames, ...listedNames, ...wranglerNames])];
2084
2248
  if (domainNames.length === 0) {
2085
2249
  return [resourceOperation("cloudflare", "pages-custom-domain", projectName, "missing", { projectName })];
@@ -2095,13 +2259,16 @@ function normalizePagesDeploymentId(deployment) {
2095
2259
  function normalizePagesDeployments(value) {
2096
2260
  return (Array.isArray(value) ? value : Array.isArray(value?.result) ? value.result : []).filter((entry) => normalizePagesDeploymentId(entry));
2097
2261
  }
2098
- function listPagesDeploymentsWithApi(projectName, { env }) {
2262
+ function pagesDeploymentEnvironments(environment = "all") {
2263
+ return environment === "preview" ? ["preview"] : environment === "production" ? ["production"] : ["preview", "production"];
2264
+ }
2265
+ function listPagesDeploymentsWithApi(projectName, { env, environment = "all" }) {
2099
2266
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
2100
2267
  if (!projectName || !accountId) {
2101
2268
  return [];
2102
2269
  }
2103
2270
  const deployments = [];
2104
- for (const pagesEnvironment of ["preview", "production"]) {
2271
+ for (const pagesEnvironment of pagesDeploymentEnvironments(environment)) {
2105
2272
  let page = 1;
2106
2273
  let totalPages = 1;
2107
2274
  while (page <= totalPages && page <= 50) {
@@ -2120,9 +2287,9 @@ function listPagesDeploymentsWithApi(projectName, { env }) {
2120
2287
  }
2121
2288
  return deployments;
2122
2289
  }
2123
- function listPagesDeployments(tenantRoot, projectName, { env }) {
2290
+ function listPagesDeployments(tenantRoot, projectName, { env, environment = "all" }) {
2124
2291
  const deployments = [];
2125
- for (const pagesEnvironment of ["preview", "production"]) {
2292
+ for (const pagesEnvironment of pagesDeploymentEnvironments(environment)) {
2126
2293
  const result = runWrangler(["pages", "deployment", "list", "--project-name", projectName, "--environment", pagesEnvironment, "--json"], {
2127
2294
  cwd: tenantRoot,
2128
2295
  allowFailure: true,
@@ -2141,14 +2308,14 @@ function listPagesDeployments(tenantRoot, projectName, { env }) {
2141
2308
  const byId = new Map(deployments.map((deployment) => [normalizePagesDeploymentId(deployment), deployment]));
2142
2309
  return [...byId.values()];
2143
2310
  }
2144
- return listPagesDeploymentsWithApi(projectName, { env });
2311
+ return listPagesDeploymentsWithApi(projectName, { env, environment });
2145
2312
  }
2146
- function deletePagesDeployments(tenantRoot, projectName, { env, dryRun }) {
2313
+ function deletePagesDeployments(tenantRoot, projectName, { env, dryRun, environment = "all" }) {
2147
2314
  if (!projectName) {
2148
2315
  return resourceOperation("cloudflare", "pages-deployments", projectName, "missing");
2149
2316
  }
2150
2317
  if (dryRun) {
2151
- return resourceOperation("cloudflare", "pages-deployments", projectName, "planned", { reason: "project_delete_prerequisite" });
2318
+ return resourceOperation("cloudflare", "pages-deployments", projectName, "planned", { reason: "project_delete_prerequisite", environment });
2152
2319
  }
2153
2320
  const accountId = env?.CLOUDFLARE_ACCOUNT_ID ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
2154
2321
  if (!accountId) {
@@ -2158,7 +2325,7 @@ function deletePagesDeployments(tenantRoot, projectName, { env, dryRun }) {
2158
2325
  let skipped = 0;
2159
2326
  let total = 0;
2160
2327
  for (let batch = 0; batch < 100; batch += 1) {
2161
- const deployments = listPagesDeployments(tenantRoot, projectName, { env });
2328
+ const deployments = listPagesDeployments(tenantRoot, projectName, { env, environment });
2162
2329
  if (deployments.length === 0) {
2163
2330
  return resourceOperation("cloudflare", "pages-deployments", projectName, deleted > 0 ? "deleted" : "missing", {
2164
2331
  deleted,
@@ -2286,31 +2453,31 @@ function configuredRailwayDestroyTargets(tenantRoot, deployConfig, scope) {
2286
2453
  identity = { deploymentKey: deployConfig.slug ?? deployConfig.name ?? "treeseed" };
2287
2454
  }
2288
2455
  const services = [];
2289
- for (const serviceKey of ["api", "marketOperationsRunner"]) {
2456
+ for (const serviceKey of ["api", "operationsRunner"]) {
2290
2457
  const service = deployConfig.services?.[serviceKey];
2291
2458
  if (!service || service.enabled === false || (service.provider ?? "railway") !== "railway") {
2292
2459
  continue;
2293
2460
  }
2294
- const baseServiceName = service.railway?.serviceName ?? `${identity.deploymentKey}-${serviceKey === "marketOperationsRunner" ? "market-operations-runner" : serviceKey}`;
2295
- const runnerPool = serviceKey === "marketOperationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
2296
- const count = serviceKey === "marketOperationsRunner" ? Math.max(1, Number.parseInt(String(runnerPool?.bootstrapCount ?? 1), 10) || 1) : 1;
2461
+ const baseServiceName = service.railway?.serviceName ?? `${identity.deploymentKey}-${serviceKey === "operationsRunner" ? "operations-runner" : serviceKey}`;
2462
+ const runnerPool = serviceKey === "operationsRunner" && service.railway?.runnerPool && typeof service.railway.runnerPool === "object" ? service.railway.runnerPool : null;
2463
+ const count = serviceKey === "operationsRunner" ? Math.max(1, Number.parseInt(String(runnerPool?.bootstrapCount ?? 1), 10) || 1) : 1;
2297
2464
  for (let index = 1; index <= count; index += 1) {
2298
- const serviceName = serviceKey === "marketOperationsRunner" ? `${String(baseServiceName).replace(/-\d+$/u, "")}-${String(index).padStart(2, "0")}` : baseServiceName;
2465
+ const serviceName = serviceKey === "operationsRunner" ? `${String(baseServiceName).replace(/-\d+$/u, "").replace(/-\d{2}$/u, "")}-${String(index).padStart(2, "0")}` : baseServiceName;
2299
2466
  services.push({
2300
2467
  key: serviceKey,
2301
2468
  projectName: service.railway?.projectName ?? identity.deploymentKey,
2302
2469
  serviceName,
2303
2470
  railwayEnvironment: normalizeRailwayEnvironmentName(service.environments?.[normalizedScope]?.railwayEnvironment ?? normalizedScope),
2304
2471
  domain: service.environments?.[normalizedScope]?.domain ?? null,
2305
- volumeMountPath: serviceKey === "marketOperationsRunner" ? service.railway?.volumeMountPath ?? runnerPool?.volumeMountPath ?? "/data" : null
2472
+ volumeMountPath: serviceKey === "operationsRunner" ? service.railway?.volumeMountPath ?? runnerPool?.volumeMountPath ?? "/data" : null
2306
2473
  });
2307
2474
  }
2308
2475
  }
2309
- const marketDatabase = deployConfig.services?.marketDatabase;
2310
- if (marketDatabase?.enabled !== false && marketDatabase?.provider === "railway" && marketDatabase?.railway?.resourceType === "postgres") {
2311
- const baseName = typeof marketDatabase.railway?.serviceName === "string" && marketDatabase.railway.serviceName.trim() ? marketDatabase.railway.serviceName.trim() : `${deployConfig.slug ?? "treeseed-market"}-postgres`;
2476
+ const treeseedDatabase = deployConfig.services?.treeseedDatabase;
2477
+ if (treeseedDatabase?.enabled !== false && treeseedDatabase?.provider === "railway" && treeseedDatabase?.railway?.resourceType === "postgres") {
2478
+ const baseName = typeof treeseedDatabase.railway?.serviceName === "string" && treeseedDatabase.railway.serviceName.trim() ? treeseedDatabase.railway.serviceName.trim() : `${deployConfig.slug ?? "treeseed-market"}-postgres`;
2312
2479
  services.push({
2313
- key: "marketDatabase",
2480
+ key: "treeseedDatabase",
2314
2481
  projectName: deployConfig.services?.api?.railway?.projectName ?? identity.deploymentKey,
2315
2482
  serviceName: `${baseName.replace(/-(staging|prod|production)$/u, "")}-${normalizedScope === "prod" ? "prod" : normalizedScope}`,
2316
2483
  railwayEnvironment: normalizeRailwayEnvironmentName(normalizedScope),
@@ -2465,6 +2632,87 @@ function killPidFromFile(filePath, { dryRun }) {
2465
2632
  }
2466
2633
  return resourceOperation("local", "dev-process", String(pid), "deleted", { pidFile: filePath });
2467
2634
  }
2635
+ const LOCAL_DOCKER_RESOURCE_PATTERN = /(?:^|[-_.])(?:treeseed|treedx|treedb)(?:[-_.]|$)|(?:treeseed|treedx|treedb)/iu;
2636
+ let destroyDockerRunnerForTests = null;
2637
+ function setDestroyDockerRunnerForTests(runner) {
2638
+ destroyDockerRunnerForTests = runner;
2639
+ }
2640
+ function runDestroyDocker(args) {
2641
+ if (destroyDockerRunnerForTests) {
2642
+ return destroyDockerRunnerForTests(args);
2643
+ }
2644
+ return spawnSync("docker", args, { encoding: "utf8", stdio: "pipe" });
2645
+ }
2646
+ function dockerAvailable() {
2647
+ const result = runDestroyDocker(["info"]);
2648
+ return result.status === 0;
2649
+ }
2650
+ function dockerList(formatArgs) {
2651
+ const result = runDestroyDocker(formatArgs);
2652
+ if (result.status !== 0) {
2653
+ return [];
2654
+ }
2655
+ return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
2656
+ }
2657
+ function matchingDockerEntries(lines, parser) {
2658
+ return lines.map(parser).filter((entry) => entry && LOCAL_DOCKER_RESOURCE_PATTERN.test(`${entry.name} ${entry.image ?? ""}`));
2659
+ }
2660
+ function removeDockerResource(kind, id, name) {
2661
+ const args = kind === "container" ? ["rm", "-f", id] : kind === "volume" ? ["volume", "rm", "-f", id] : ["network", "rm", id];
2662
+ const result = runDestroyDocker(args);
2663
+ if (result.status === 0) {
2664
+ return resourceOperation("local", `docker-${kind}`, name, "deleted", { id });
2665
+ }
2666
+ return resourceOperation("local", `docker-${kind}`, name, "blocked", {
2667
+ id,
2668
+ reason: result.stderr?.trim() || result.stdout?.trim() || "docker_remove_failed"
2669
+ });
2670
+ }
2671
+ function dockerLocalRuntimeResourceOperations({ dryRun = false } = {}) {
2672
+ if (!dockerAvailable()) {
2673
+ return [resourceOperation("local", "docker-cleanup", "docker", "skipped", { reason: "docker_unavailable" })];
2674
+ }
2675
+ const containers = matchingDockerEntries(
2676
+ dockerList(["ps", "-a", "--format", "{{.ID}} {{.Names}} {{.Image}}"]),
2677
+ (line) => {
2678
+ const [id, name, image] = line.split(" ");
2679
+ return id && name ? { id, name, image } : null;
2680
+ }
2681
+ );
2682
+ const volumes = matchingDockerEntries(
2683
+ dockerList(["volume", "ls", "--format", "{{.Name}}"]),
2684
+ (line) => ({ id: line, name: line })
2685
+ );
2686
+ const networks = matchingDockerEntries(
2687
+ dockerList(["network", "ls", "--format", "{{.ID}} {{.Name}}"]),
2688
+ (line) => {
2689
+ const [id, name] = line.split(" ");
2690
+ return id && name ? { id, name } : null;
2691
+ }
2692
+ ).filter((entry) => !["bridge", "host", "none"].includes(entry.name));
2693
+ if (dryRun) {
2694
+ return [
2695
+ ...containers.map((entry) => resourceOperation("local", "docker-container", entry.name, "planned", { id: entry.id })),
2696
+ ...volumes.map((entry) => resourceOperation("local", "docker-volume", entry.name, "planned", { id: entry.id })),
2697
+ ...networks.map((entry) => resourceOperation("local", "docker-network", entry.name, "planned", { id: entry.id })),
2698
+ ...containers.length || volumes.length || networks.length ? [] : [resourceOperation("local", "docker-cleanup", "docker", "missing", { reason: "no_matching_resources" })]
2699
+ ];
2700
+ }
2701
+ const operations = [];
2702
+ for (const entry of containers) {
2703
+ operations.push(removeDockerResource("container", entry.id, entry.name));
2704
+ }
2705
+ for (const entry of volumes) {
2706
+ operations.push(removeDockerResource("volume", entry.id, entry.name));
2707
+ }
2708
+ for (const entry of networks) {
2709
+ operations.push(removeDockerResource("network", entry.id, entry.name));
2710
+ }
2711
+ if (!operations.length) {
2712
+ operations.push(resourceOperation("local", "docker-cleanup", "docker", "missing", { reason: "no_matching_resources" }));
2713
+ }
2714
+ return operations;
2715
+ }
2468
2716
  function destroyLocalRuntimeResources(tenantRoot, { dryRun = false, deleteData = false } = {}) {
2469
2717
  const operations = [];
2470
2718
  const pidDir = resolve(tenantRoot, ".treeseed/dev-pids");
@@ -2481,7 +2729,7 @@ function destroyLocalRuntimeResources(tenantRoot, { dryRun = false, deleteData =
2481
2729
  for (const relativePath of [
2482
2730
  ".treeseed/generated/environments/local",
2483
2731
  ".treeseed/generated/dev",
2484
- ".treeseed/market-operations-runner",
2732
+ ".treeseed/operations-runner",
2485
2733
  ".treeseed/local-capacity-provider/data"
2486
2734
  ]) {
2487
2735
  const absolutePath = resolve(tenantRoot, relativePath);
@@ -2496,11 +2744,250 @@ function destroyLocalRuntimeResources(tenantRoot, { dryRun = false, deleteData =
2496
2744
  rmSync(absolutePath, { recursive: true, force: true });
2497
2745
  operations.push(resourceOperation("local", "data-path", relativePath, "deleted"));
2498
2746
  }
2747
+ operations.push(...dockerLocalRuntimeResourceOperations({ dryRun }));
2499
2748
  } else {
2500
2749
  operations.push(resourceOperation("local", "data-path", ".treeseed/generated/environments/local", "skipped", { reason: "data_preserved" }));
2501
2750
  }
2502
2751
  return { operations };
2503
2752
  }
2753
+ function treeSeedSweepTokens(deployConfig, state) {
2754
+ const configuredHosts = [
2755
+ deployConfig.siteUrl,
2756
+ deployConfig.surfaces?.web?.publicBaseUrl,
2757
+ deployConfig.surfaces?.web?.environments?.staging?.domain,
2758
+ deployConfig.surfaces?.web?.environments?.prod?.domain,
2759
+ deployConfig.surfaces?.api?.environments?.staging?.domain,
2760
+ deployConfig.surfaces?.api?.environments?.prod?.domain,
2761
+ deployConfig.services?.api?.environments?.staging?.domain,
2762
+ deployConfig.services?.api?.environments?.prod?.domain
2763
+ ].map((value) => primaryHost(value) ?? value);
2764
+ return [...new Set([
2765
+ "treeseed",
2766
+ deployConfig.slug,
2767
+ deployConfig.name,
2768
+ state.identity?.deploymentKey,
2769
+ state.identity?.environmentKey,
2770
+ state.pages?.projectName,
2771
+ state.workerName,
2772
+ state.content?.bucketName,
2773
+ state.queues?.agentWork?.name,
2774
+ state.queues?.agentWork?.dlqName,
2775
+ state.kvNamespaces?.FORM_GUARD_KV?.name,
2776
+ state.kvNamespaces?.SESSION?.name,
2777
+ state.d1Databases?.SITE_DATA_DB?.databaseName,
2778
+ ...configuredHosts
2779
+ ].map((value) => String(value ?? "").trim().toLowerCase()).filter((value) => value.length >= 4))];
2780
+ }
2781
+ function isProtectedAiIntegrationResource(value) {
2782
+ return /(?:^|[-_.])(?:ai-gateway|workers-ai|ai-integration|openai|anthropic)(?:[-_.]|$)/iu.test(String(value ?? ""));
2783
+ }
2784
+ function matchesTreeSeedSweep(value, tokens) {
2785
+ const normalized = String(value ?? "").trim().toLowerCase();
2786
+ if (!normalized || isProtectedAiIntegrationResource(normalized)) {
2787
+ return false;
2788
+ }
2789
+ return tokens.some((token) => normalized === token || normalized.includes(token));
2790
+ }
2791
+ function cloudflareNameCandidates(entry) {
2792
+ return [
2793
+ entry?.name,
2794
+ entry?.title,
2795
+ entry?.id,
2796
+ entry?.queue_name,
2797
+ entry?.script,
2798
+ entry?.domain,
2799
+ entry?.hostname,
2800
+ entry?.content,
2801
+ entry?.comment,
2802
+ ...Array.isArray(entry?.domains) ? entry.domains : [],
2803
+ ...Array.isArray(entry?.tags) ? entry.tags : []
2804
+ ].filter(Boolean);
2805
+ }
2806
+ function cloudflareEntryMatchesTreeSeed(entry, tokens) {
2807
+ return cloudflareNameCandidates(entry).some((candidate) => matchesTreeSeedSweep(candidate, tokens));
2808
+ }
2809
+ function deleteDnsRecord(zoneId, record, { env, dryRun }) {
2810
+ const name = record?.name ?? record?.content ?? record?.id ?? null;
2811
+ if (!zoneId || !record?.id) {
2812
+ return resourceOperation("cloudflare", "dns-record", name, "missing", { zoneId });
2813
+ }
2814
+ if (dryRun) {
2815
+ return resourceOperation("cloudflare", "dns-record", name, "planned", {
2816
+ zoneId,
2817
+ id: record.id,
2818
+ content: record.content ?? null,
2819
+ recordType: record.type ?? null
2820
+ });
2821
+ }
2822
+ return deleteCloudflareApiResource(
2823
+ `/zones/${encodeURIComponent(zoneId)}/dns_records/${encodeURIComponent(record.id)}`,
2824
+ { env, dryRun: false, name, type: "dns-record" }
2825
+ );
2826
+ }
2827
+ function sweepTreeSeedCloudflareResources(tenantRoot, deployConfig, state, { env, dryRun, deleteData }) {
2828
+ const tokens = treeSeedSweepTokens(deployConfig, state);
2829
+ const operations = [];
2830
+ const pagesProjects = listPagesProjects(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens));
2831
+ for (const project of pagesProjects) {
2832
+ const projectName = project?.name ?? project?.id ?? null;
2833
+ operations.push(...deletePagesCustomDomains(tenantRoot, projectName, [], { env, dryRun, knownOnly: false }));
2834
+ operations.push(deletePagesDeployments(tenantRoot, projectName, { env, dryRun, environment: "all" }));
2835
+ operations.push(deletePagesProject(projectName, { env, dryRun }));
2836
+ }
2837
+ for (const worker of listWorkers(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens))) {
2838
+ const name = worker?.id ?? worker?.name ?? worker?.script ?? null;
2839
+ const deleted = deleteWorker(tenantRoot, name, { env, dryRun, force: true });
2840
+ operations.push(resourceOperation("cloudflare", "worker", name, deleted.status, { ...deleted, sweep: true }));
2841
+ }
2842
+ for (const namespace of listKvNamespaces(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens))) {
2843
+ const deleted = deleteKvNamespace(tenantRoot, namespace.id, { env, dryRun });
2844
+ operations.push(resourceOperation("cloudflare", "kv-namespace", namespace.title ?? namespace.id, deleted.status, { ...deleted, sweep: true }));
2845
+ }
2846
+ for (const queue of listQueues(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens))) {
2847
+ operations.push({ ...deleteQueueByName(tenantRoot, queue, { env, dryRun }), sweep: true });
2848
+ }
2849
+ for (const database of listD1Databases(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens))) {
2850
+ const name = database?.name ?? database?.uuid ?? database?.id ?? null;
2851
+ const deleted = deleteData ? deleteD1Database(tenantRoot, name, { env, dryRun }) : null;
2852
+ operations.push(resourceOperation("cloudflare", "d1-database", name, deleteData ? deleted?.status : "skipped", {
2853
+ ...deleteData ? deleted : { reason: "data_preserved" },
2854
+ sweep: true
2855
+ }));
2856
+ }
2857
+ for (const bucket of listR2Buckets(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens))) {
2858
+ operations.push({ ...deleteR2Bucket(tenantRoot, bucket.name, { env, dryRun, deleteData }), sweep: true });
2859
+ }
2860
+ for (const widget of listTurnstileWidgets(tenantRoot, env).filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens))) {
2861
+ const deleted = deleteTurnstileWidget(widget.sitekey, { env, dryRun, name: widget.name });
2862
+ operations.push(resourceOperation("cloudflare", "turnstile-widget", widget.name ?? widget.sitekey, deleted.status, { ...deleted, sweep: true }));
2863
+ }
2864
+ for (const zone of listDnsZones(env)) {
2865
+ const zoneId = zone?.id ?? null;
2866
+ for (const record of listDnsRecords(zoneId, env)) {
2867
+ if (record?.type === "SOA" || record?.type === "NS") {
2868
+ continue;
2869
+ }
2870
+ if (!cloudflareEntryMatchesTreeSeed(record, tokens)) {
2871
+ continue;
2872
+ }
2873
+ operations.push({ ...deleteDnsRecord(zoneId, record, { env, dryRun }), zoneName: zone?.name ?? null, sweep: true });
2874
+ }
2875
+ }
2876
+ return operations.length > 0 ? operations : [resourceOperation("cloudflare", "treeseed-sweep", "cloudflare", "missing", { reason: "no_matching_resources" })];
2877
+ }
2878
+ function countMatchingCloudflareEntries(entries, tokens) {
2879
+ return entries.filter((entry) => cloudflareEntryMatchesTreeSeed(entry, tokens)).length;
2880
+ }
2881
+ function cloudflareDestroyVerification(tenantRoot, deployConfig, state, env) {
2882
+ const tokens = treeSeedSweepTokens(deployConfig, state);
2883
+ const zoneIds = /* @__PURE__ */ new Set([
2884
+ deployConfig.cloudflare?.zoneId,
2885
+ state.webCache?.webZoneId,
2886
+ state.webCache?.contentZoneId
2887
+ ]);
2888
+ for (const zone of listDnsZones(env)) {
2889
+ if (zone?.id) {
2890
+ zoneIds.add(zone.id);
2891
+ }
2892
+ }
2893
+ const dnsRecords = [];
2894
+ for (const zoneId of [...zoneIds].filter(Boolean)) {
2895
+ dnsRecords.push(...listDnsRecords(zoneId, env));
2896
+ }
2897
+ const remaining = {
2898
+ pages: countMatchingCloudflareEntries(listPagesProjects(tenantRoot, env), tokens),
2899
+ workers: countMatchingCloudflareEntries(listWorkers(tenantRoot, env), tokens),
2900
+ kvNamespaces: countMatchingCloudflareEntries(listKvNamespaces(tenantRoot, env), tokens),
2901
+ queues: countMatchingCloudflareEntries(listQueues(tenantRoot, env), tokens),
2902
+ d1Databases: countMatchingCloudflareEntries(listD1Databases(tenantRoot, env), tokens),
2903
+ r2Buckets: countMatchingCloudflareEntries(listR2Buckets(tenantRoot, env), tokens),
2904
+ turnstileWidgets: countMatchingCloudflareEntries(listTurnstileWidgets(tenantRoot, env), tokens),
2905
+ dnsRecords: countMatchingCloudflareEntries(
2906
+ dnsRecords.filter((record) => record?.type !== "SOA" && record?.type !== "NS"),
2907
+ tokens
2908
+ )
2909
+ };
2910
+ const totalRemaining = Object.values(remaining).reduce((sum, value) => sum + value, 0);
2911
+ return {
2912
+ provider: "cloudflare",
2913
+ method: "cloudflare-api",
2914
+ status: totalRemaining === 0 ? "clean" : "remaining",
2915
+ remaining,
2916
+ totalRemaining
2917
+ };
2918
+ }
2919
+ function localDockerDestroyVerification() {
2920
+ if (!dockerAvailable()) {
2921
+ return {
2922
+ provider: "local-docker",
2923
+ method: "docker-cli",
2924
+ status: "skipped",
2925
+ reason: "docker_unavailable",
2926
+ remaining: {
2927
+ containers: 0,
2928
+ volumes: 0,
2929
+ networks: 0
2930
+ },
2931
+ totalRemaining: 0
2932
+ };
2933
+ }
2934
+ const containers = matchingDockerEntries(
2935
+ dockerList(["ps", "-a", "--format", "{{.ID}} {{.Names}} {{.Image}}"]),
2936
+ (line) => {
2937
+ const [id, name, image] = line.split(" ");
2938
+ return id && name ? { id, name, image } : null;
2939
+ }
2940
+ ).length;
2941
+ const volumes = matchingDockerEntries(
2942
+ dockerList(["volume", "ls", "--format", "{{.Name}}"]),
2943
+ (line) => ({ id: line, name: line })
2944
+ ).length;
2945
+ const networks = matchingDockerEntries(
2946
+ dockerList(["network", "ls", "--format", "{{.ID}} {{.Name}}"]),
2947
+ (line) => {
2948
+ const [id, name] = line.split(" ");
2949
+ return id && name ? { id, name } : null;
2950
+ }
2951
+ ).filter((entry) => !["bridge", "host", "none"].includes(entry.name)).length;
2952
+ const remaining = { containers, volumes, networks };
2953
+ const totalRemaining = containers + volumes + networks;
2954
+ return {
2955
+ provider: "local-docker",
2956
+ method: "docker-cli",
2957
+ status: totalRemaining === 0 ? "clean" : "remaining",
2958
+ remaining,
2959
+ totalRemaining
2960
+ };
2961
+ }
2962
+ async function sweepTreeSeedRailwayResources(deployConfig, state, { env, dryRun }) {
2963
+ if (!resolveRailwayApiToken(env)) {
2964
+ return [resourceOperation("railway", "treeseed-sweep", "railway", "blocked", { reason: "missing_railway_api_token" })];
2965
+ }
2966
+ const tokens = treeSeedSweepTokens(deployConfig, state);
2967
+ const workspace = await resolveRailwayWorkspaceContext({ env, workspace: resolveRailwayWorkspace(env) });
2968
+ const projects = await listRailwayProjects({ env, workspaceId: workspace.id });
2969
+ const operations = [];
2970
+ for (const project of projects) {
2971
+ if (project.deletedAt || !matchesTreeSeedSweep(project.name, tokens)) {
2972
+ continue;
2973
+ }
2974
+ if (dryRun) {
2975
+ operations.push(resourceOperation("railway", "project", project.name, "planned", {
2976
+ id: project.id,
2977
+ workspaceId: workspace.id,
2978
+ sweep: true
2979
+ }));
2980
+ } else {
2981
+ const deleted = await deleteRailwayProject({ projectId: project.id, env });
2982
+ operations.push(resourceOperation("railway", "project", project.name, deleted.status, {
2983
+ id: project.id,
2984
+ workspaceId: workspace.id,
2985
+ sweep: true
2986
+ }));
2987
+ }
2988
+ }
2989
+ return operations.length > 0 ? operations : [resourceOperation("railway", "treeseed-sweep", "railway", "missing", { reason: "no_matching_projects" })];
2990
+ }
2504
2991
  async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2505
2992
  const target = normalizeTarget(options.scope ?? options.target ?? "prod");
2506
2993
  const deployConfig = loadTenantDeployConfig(tenantRoot);
@@ -2512,6 +2999,8 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2512
2999
  const dryRun = options.dryRun ?? false;
2513
3000
  const deleteData = options.deleteData === true;
2514
3001
  const force = options.force ?? false;
3002
+ const sweepTreeseed = options.sweepTreeseed === true;
3003
+ const destroysSharedWebSurface = target.kind === "persistent" && target.scope === "prod" && deleteData;
2515
3004
  const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
2516
3005
  const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
2517
3006
  const queues = dryRun ? [] : listQueues(tenantRoot, env);
@@ -2585,11 +3074,21 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2585
3074
  ].filter(Boolean);
2586
3075
  const dnsRecords = [.../* @__PURE__ */ new Set([...pageDnsNames, ...apiDnsNames])].flatMap((name) => deleteDnsRecordsForName(deployConfig, name, { env, dryRun }));
2587
3076
  const cacheRules = deleteTreeseedCacheRules(deployConfig, state, { env, dryRun });
2588
- const pageCustomDomains = pagesProject || dryRun ? deletePagesCustomDomains(tenantRoot, state.pages?.projectName, pageDnsNames, { env, dryRun }) : [resourceOperation("cloudflare", "pages-custom-domain", state.pages?.projectName, "missing")];
2589
- const pageDeployments = pagesProject || dryRun ? deletePagesDeployments(tenantRoot, state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-deployments", state.pages?.projectName, "missing");
2590
- const pages = pagesProject || dryRun ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "missing");
3077
+ const pageCustomDomains = pagesProject || dryRun ? deletePagesCustomDomains(tenantRoot, state.pages?.projectName, pageDnsNames, { env, dryRun, knownOnly: !destroysSharedWebSurface }) : [resourceOperation("cloudflare", "pages-custom-domain", state.pages?.projectName, "missing")];
3078
+ const pageDeployments = pagesProject || dryRun ? deletePagesDeployments(tenantRoot, state.pages?.projectName, {
3079
+ env,
3080
+ dryRun,
3081
+ environment: destroysSharedWebSurface ? "all" : "preview"
3082
+ }) : resourceOperation("cloudflare", "pages-deployments", state.pages?.projectName, "missing");
3083
+ const pages = destroysSharedWebSurface && (pagesProject || dryRun) ? deletePagesProject(state.pages?.projectName, { env, dryRun }) : resourceOperation("cloudflare", "pages-project", state.pages?.projectName, "skipped", {
3084
+ reason: target.scope === "prod" ? "delete_data_required" : "shared_web_surface"
3085
+ });
2591
3086
  const local = target.kind === "persistent" && target.scope === "local" ? destroyLocalRuntimeResources(tenantRoot, { dryRun, deleteData }) : { operations: [] };
2592
3087
  const railway = await destroyRailwayResources(tenantRoot, deployConfig, target, { dryRun, deleteData, env: process.env });
3088
+ const sweep = sweepTreeseed ? {
3089
+ cloudflare: sweepTreeSeedCloudflareResources(tenantRoot, deployConfig, state, { env, dryRun, deleteData }),
3090
+ railway: await sweepTreeSeedRailwayResources(deployConfig, state, { env: process.env, dryRun })
3091
+ } : { cloudflare: [], railway: [] };
2593
3092
  const operations = {
2594
3093
  cloudflare: [
2595
3094
  resourceOperation("cloudflare", "worker", state.workerName, workerResult.status, workerResult),
@@ -2608,16 +3107,26 @@ async function destroyTreeseedEnvironmentResources(tenantRoot, options = {}) {
2608
3107
  pageDeployments,
2609
3108
  pages,
2610
3109
  ...dnsRecords,
2611
- ...cacheRules
3110
+ ...cacheRules,
3111
+ ...sweep.cloudflare
3112
+ ],
3113
+ railway: [
3114
+ ...railway.operations,
3115
+ ...sweep.railway
2612
3116
  ],
2613
- railway: railway.operations,
2614
3117
  local: local.operations
2615
3118
  };
3119
+ const verification = dryRun ? null : {
3120
+ cloudflare: cloudflareDestroyVerification(tenantRoot, deployConfig, state, env),
3121
+ ...target.kind === "persistent" && target.scope === "local" ? { localDocker: localDockerDestroyVerification() } : {}
3122
+ };
2616
3123
  return {
2617
3124
  target,
2618
3125
  deleteData,
3126
+ sweepTreeseed,
2619
3127
  summary: buildDestroySummary(deployConfig, state, target),
2620
- operations
3128
+ operations,
3129
+ verification
2621
3130
  };
2622
3131
  }
2623
3132
  function destroyCloudflareResources(tenantRoot, options = {}) {
@@ -3243,6 +3752,7 @@ export {
3243
3752
  buildWranglerConfigContents,
3244
3753
  cleanupDestroyedState,
3245
3754
  cloudflareApiRequest,
3755
+ cloudflareDestroyVerification,
3246
3756
  collectMissingDeployInputs,
3247
3757
  createBranchPreviewDeployTarget,
3248
3758
  createPersistentDeployTarget,
@@ -3251,17 +3761,21 @@ export {
3251
3761
  deriveTreeseedStagingSurfaceDomain,
3252
3762
  destroyCloudflareResources,
3253
3763
  destroyTreeseedEnvironmentResources,
3764
+ dockerLocalRuntimeResourceOperations,
3254
3765
  ensureGeneratedWranglerConfig,
3255
3766
  finalizeDeploymentState,
3256
3767
  getTurnstileWidget,
3257
3768
  hasProvisionedCloudflareResources,
3258
3769
  isWranglerAlreadyExistsError,
3259
3770
  listD1Databases,
3771
+ listDnsRecords,
3772
+ listDnsZones,
3260
3773
  listKvNamespaces,
3261
3774
  listPagesProjects,
3262
3775
  listQueues,
3263
3776
  listR2Buckets,
3264
3777
  listTurnstileWidgets,
3778
+ listWorkers,
3265
3779
  loadDeployState,
3266
3780
  markDeploymentInitialized,
3267
3781
  markManagedServicesInitialized,
@@ -3286,6 +3800,7 @@ export {
3286
3800
  runRemoteD1Migrations,
3287
3801
  runWrangler,
3288
3802
  scopeFromTarget,
3803
+ setDestroyDockerRunnerForTests,
3289
3804
  shouldDeleteRailwayProjectAfterEnvironmentDestroy,
3290
3805
  syncCloudflareSecrets,
3291
3806
  updateTurnstileWidget,