@treeseed/sdk 0.10.27 → 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 (168) 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 +11 -4
  20. package/dist/index.js +71 -7
  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/template-registry.js +14 -7
  67. package/dist/operations/services/verification-cache.d.ts +25 -0
  68. package/dist/operations/services/verification-cache.js +71 -0
  69. package/dist/operations/services/workspace-dependency-mode.js +9 -1
  70. package/dist/operations/services/workspace-save.js +1 -1
  71. package/dist/operations/services/workspace-tools.js +2 -1
  72. package/dist/platform/contracts.d.ts +32 -1
  73. package/dist/platform/deploy-config.js +73 -8
  74. package/dist/platform/env.yaml +163 -35
  75. package/dist/platform/environment.d.ts +1 -0
  76. package/dist/platform/environment.js +74 -5
  77. package/dist/platform/plugin.d.ts +9 -0
  78. package/dist/platform-operation-store.js +2 -2
  79. package/dist/platform-operations.js +1 -1
  80. package/dist/reconcile/bootstrap-systems.js +2 -2
  81. package/dist/reconcile/builtin-adapters.js +372 -189
  82. package/dist/reconcile/contracts.d.ts +9 -5
  83. package/dist/reconcile/desired-state.d.ts +1 -0
  84. package/dist/reconcile/desired-state.js +5 -5
  85. package/dist/reconcile/engine.d.ts +5 -2
  86. package/dist/reconcile/engine.js +53 -32
  87. package/dist/reconcile/index.d.ts +2 -0
  88. package/dist/reconcile/index.js +2 -0
  89. package/dist/reconcile/live-acceptance.d.ts +79 -0
  90. package/dist/reconcile/live-acceptance.js +1615 -0
  91. package/dist/reconcile/platform.d.ts +104 -0
  92. package/dist/reconcile/platform.js +100 -0
  93. package/dist/reconcile/state.js +4 -4
  94. package/dist/reconcile/units.js +2 -2
  95. package/dist/scripts/deployment-readiness.js +20 -0
  96. package/dist/scripts/generate-treedx-openapi-types.js +186 -0
  97. package/dist/scripts/operations-runner-smoke.js +16 -0
  98. package/dist/scripts/release-verify.js +4 -1
  99. package/dist/scripts/template-catalog.test.js +7 -7
  100. package/dist/scripts/tenant-workflow-action.js +10 -1
  101. package/dist/sdk-types.d.ts +172 -5
  102. package/dist/sdk-types.js +28 -3
  103. package/dist/sdk.d.ts +35 -24
  104. package/dist/sdk.js +186 -17
  105. package/dist/template-launch-requirements.js +9 -0
  106. package/dist/treedx/adapters.d.ts +6 -0
  107. package/dist/treedx/adapters.js +36 -0
  108. package/dist/treedx/client.d.ts +222 -0
  109. package/dist/treedx/client.js +871 -0
  110. package/dist/treedx/errors.d.ts +13 -0
  111. package/dist/treedx/errors.js +17 -0
  112. package/dist/treedx/federated-client.d.ts +27 -0
  113. package/dist/treedx/federated-client.js +158 -0
  114. package/dist/treedx/generated/openapi-types.d.ts +3558 -0
  115. package/dist/treedx/generated/openapi-types.js +0 -0
  116. package/dist/treedx/graph-adapter.d.ts +33 -0
  117. package/dist/treedx/graph-adapter.js +156 -0
  118. package/dist/treedx/index.d.ts +14 -0
  119. package/dist/treedx/index.js +48 -0
  120. package/dist/treedx/market-integration.d.ts +27 -0
  121. package/dist/treedx/market-integration.js +131 -0
  122. package/dist/treedx/ports.d.ts +166 -0
  123. package/dist/treedx/ports.js +231 -0
  124. package/dist/treedx/query-adapter.d.ts +19 -0
  125. package/dist/treedx/query-adapter.js +62 -0
  126. package/dist/treedx/registry-client.d.ts +11 -0
  127. package/dist/treedx/registry-client.js +19 -0
  128. package/dist/treedx/repository-adapter.d.ts +45 -0
  129. package/dist/treedx/repository-adapter.js +308 -0
  130. package/dist/treedx/sdk-integration.d.ts +27 -0
  131. package/dist/treedx/sdk-integration.js +63 -0
  132. package/dist/treedx/types.d.ts +1084 -0
  133. package/dist/treedx/types.js +8 -0
  134. package/dist/treedx/workspace-adapter.d.ts +27 -0
  135. package/dist/treedx/workspace-adapter.js +65 -0
  136. package/dist/treedx-backends.d.ts +218 -0
  137. package/dist/treedx-backends.js +632 -0
  138. package/dist/treedx-client.d.ts +86 -0
  139. package/dist/treedx-client.js +175 -0
  140. package/dist/treeseed/template-catalog/catalog.fixture.json +497 -138
  141. package/dist/workflow/operations.d.ts +119 -13
  142. package/dist/workflow/operations.js +309 -53
  143. package/dist/workflow-state.d.ts +13 -0
  144. package/dist/workflow-state.js +43 -26
  145. package/dist/workflow-support.d.ts +11 -3
  146. package/dist/workflow-support.js +67 -3
  147. package/dist/workflow.d.ts +5 -0
  148. package/drizzle/market/0004_treedx_market_integration.sql +99 -0
  149. package/package.json +34 -3
  150. package/templates/github/deploy-web.workflow.yml +39 -6
  151. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.d.ts +0 -3
  152. package/dist/treeseed/template-catalog/templates/starter-basic/template/astro.config.ts +0 -6
  153. package/dist/treeseed/template-catalog/templates/starter-basic/template/package.json +0 -35
  154. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/api/server.js +0 -4
  155. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/config.yaml +0 -65
  156. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/decisions/adopt-initial-proposal-loop.mdx +0 -22
  157. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/empty/.gitkeep +0 -1
  158. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/knowledge/handbook/index.mdx +0 -11
  159. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/pages/welcome.mdx +0 -11
  160. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/people/starter-steward.mdx +0 -11
  161. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content/proposals/establish-initial-proposal-loop.mdx +0 -17
  162. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.d.ts +0 -1
  163. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/content.config.ts +0 -3
  164. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/env.yaml +0 -1
  165. package/dist/treeseed/template-catalog/templates/starter-basic/template/src/manifest.yaml +0 -26
  166. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +0 -74
  167. package/dist/treeseed/template-catalog/templates/starter-basic/template/tsconfig.json +0 -9
  168. package/dist/treeseed/template-catalog/templates/starter-basic/template.config.json +0 -103
@@ -0,0 +1,791 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { resolveTreeseedLaunchEnvironment } from "../operations/services/config-runtime.js";
5
+ import { cloudflareApiRequest, resolveCloudflareZoneIdForHost, resolveConfiguredCloudflareAccountId, runWrangler } from "../operations/services/deploy.js";
6
+ const ALL_ENVIRONMENTS = ["local", "staging", "prod"];
7
+ const PROVIDER_ENVIRONMENTS = ["staging", "prod"];
8
+ function capabilities(ids, environments = ALL_ENVIRONMENTS) {
9
+ return ids.map((id) => ({ id, environments }));
10
+ }
11
+ function syntheticStatus(input) {
12
+ return {
13
+ status: "pending",
14
+ locators: {
15
+ hostId: input.unit.host.id,
16
+ projectGroupId: input.unit.projectGroup?.id ?? null
17
+ },
18
+ state: {
19
+ unitId: input.unit.id,
20
+ serviceType: input.unit.serviceType.id,
21
+ placement: input.unit.placement,
22
+ dryRun: input.dryRun === true
23
+ },
24
+ warnings: []
25
+ };
26
+ }
27
+ function defaultPlan(input) {
28
+ return {
29
+ unitId: input.unit.id,
30
+ action: input.observed.status === "ready" ? "noop" : "create",
31
+ reasons: input.observed.status === "ready" ? ["unit already ready"] : ["unit is not yet recorded as ready by the hosting graph"],
32
+ before: input.observed.state,
33
+ after: sanitizedUnitConfig(input.unit),
34
+ warnings: []
35
+ };
36
+ }
37
+ function defaultVerify(input) {
38
+ const hostCapabilities = new Set(input.unit.host.capabilities.filter((capability) => capability.environments.includes(input.environment)).map((capability) => capability.id));
39
+ const missing = input.unit.requiredCapabilities.filter((capability) => !hostCapabilities.has(capability));
40
+ return {
41
+ unitId: input.unit.id,
42
+ status: missing.length === 0 ? input.observed.status : "blocked",
43
+ verified: missing.length === 0,
44
+ checks: [
45
+ {
46
+ key: "host-capabilities",
47
+ label: "Host supports required capabilities",
48
+ ok: missing.length === 0,
49
+ expected: input.unit.requiredCapabilities,
50
+ observed: [...hostCapabilities],
51
+ issues: missing.map((capability) => `Missing host capability: ${capability}`)
52
+ },
53
+ {
54
+ key: "secrets-redacted",
55
+ label: "Secrets are represented by references only",
56
+ ok: !JSON.stringify(input.unit.config).match(/(token|secret|password|key)\s*[:=]\s*[^",}]+/iu),
57
+ expected: "secretRefs",
58
+ observed: input.unit.secretRefs,
59
+ issues: []
60
+ }
61
+ ],
62
+ warnings: []
63
+ };
64
+ }
65
+ function createSyntheticHostAdapter(id, label, capabilityIds, environments = ALL_ENVIRONMENTS) {
66
+ return {
67
+ id,
68
+ label,
69
+ capabilities: capabilities(capabilityIds, environments),
70
+ refresh: syntheticStatus,
71
+ diff: defaultPlan,
72
+ apply(input) {
73
+ return {
74
+ ...syntheticStatus(input),
75
+ status: input.dryRun ? "pending" : "ready",
76
+ state: {
77
+ ...syntheticStatus(input).state,
78
+ applied: input.dryRun !== true
79
+ }
80
+ };
81
+ },
82
+ verify: defaultVerify,
83
+ status: syntheticStatus
84
+ };
85
+ }
86
+ function unitConfig(input) {
87
+ return input.unit.config && typeof input.unit.config === "object" ? input.unit.config : {};
88
+ }
89
+ function cloudflarePagesConfig(input) {
90
+ return unitConfig(input).cloudflare?.pages && typeof unitConfig(input).cloudflare.pages === "object" ? unitConfig(input).cloudflare.pages : {};
91
+ }
92
+ function cloudflarePagesProjectName(input) {
93
+ const pages = cloudflarePagesConfig(input);
94
+ return typeof pages.projectName === "string" && pages.projectName.trim() ? pages.projectName.trim() : null;
95
+ }
96
+ function cloudflarePagesBranchName(input) {
97
+ const pages = cloudflarePagesConfig(input);
98
+ const key = input.environment === "prod" ? "productionBranch" : "stagingBranch";
99
+ const fallback = input.environment === "prod" ? "main" : "staging";
100
+ return typeof pages[key] === "string" && pages[key].trim() ? pages[key].trim() : fallback;
101
+ }
102
+ function cloudflarePagesBuildOutputDir(input) {
103
+ const pages = cloudflarePagesConfig(input);
104
+ return typeof pages.buildOutputDir === "string" && pages.buildOutputDir.trim() ? pages.buildOutputDir.trim() : "dist";
105
+ }
106
+ function cloudflarePagesBuildCommand(input) {
107
+ const pages = cloudflarePagesConfig(input);
108
+ return typeof pages.buildCommand === "string" && pages.buildCommand.trim() ? pages.buildCommand.trim() : null;
109
+ }
110
+ function cloudflarePagesConfigRoot(input) {
111
+ let current = resolve(input.graph.tenantRoot);
112
+ while (true) {
113
+ if (existsSync(resolve(current, ".treeseed", "config", "machine.yaml"))) {
114
+ return current;
115
+ }
116
+ const parent = dirname(current);
117
+ if (parent === current) {
118
+ return resolve(input.graph.tenantRoot);
119
+ }
120
+ current = parent;
121
+ }
122
+ }
123
+ function cloudflarePagesEnv(input) {
124
+ const configRoot = cloudflarePagesConfigRoot(input);
125
+ const resolvedValues = input.environment === "local" ? {} : resolveTreeseedLaunchEnvironment({
126
+ tenantRoot: configRoot,
127
+ scope: input.environment
128
+ });
129
+ const accountId = [
130
+ process.env.CLOUDFLARE_ACCOUNT_ID,
131
+ resolvedValues.CLOUDFLARE_ACCOUNT_ID,
132
+ resolveConfiguredCloudflareAccountId(input.graph.deployConfig)
133
+ ].find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
134
+ const token = [
135
+ process.env.CLOUDFLARE_API_TOKEN,
136
+ resolvedValues.CLOUDFLARE_API_TOKEN
137
+ ].find((value) => typeof value === "string" && value.trim().length > 0)?.trim() ?? "";
138
+ return {
139
+ ...resolvedValues,
140
+ CLOUDFLARE_ACCOUNT_ID: accountId,
141
+ CLOUDFLARE_API_TOKEN: token
142
+ };
143
+ }
144
+ function cloudflarePagesDomain(input) {
145
+ const config = unitConfig(input);
146
+ return typeof config.domain === "string" && config.domain.trim() ? config.domain.trim() : null;
147
+ }
148
+ function cloudflarePagesDeploymentUrl(projectName, branchName, environment) {
149
+ return environment === "prod" ? `https://${projectName}.pages.dev` : `https://${branchName}.${projectName}.pages.dev`;
150
+ }
151
+ function cloudflarePagesDnsTarget(projectName, branchName, environment) {
152
+ return environment === "prod" ? `${projectName}.pages.dev` : `${branchName}.${projectName}.pages.dev`;
153
+ }
154
+ function cloudflarePagesDomainName(domain) {
155
+ return typeof domain?.name === "string" ? domain.name : typeof domain?.domain === "string" ? domain.domain : typeof domain?.hostname === "string" ? domain.hostname : "";
156
+ }
157
+ function listCloudflarePagesDomains(input, projectName) {
158
+ const env = cloudflarePagesEnv(input);
159
+ const accountId = String(env.CLOUDFLARE_ACCOUNT_ID ?? "").trim();
160
+ const token = String(env.CLOUDFLARE_API_TOKEN ?? "").trim();
161
+ if (!projectName || !accountId || !token) return [];
162
+ const domains = [];
163
+ let page = 1;
164
+ let totalPages = 1;
165
+ while (page <= totalPages && page <= 50) {
166
+ const payload = cloudflareApiRequest(
167
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains?per_page=25&page=${page}`,
168
+ { env, allowFailure: true }
169
+ );
170
+ if (payload?.success === false) break;
171
+ if (Array.isArray(payload?.result)) domains.push(...payload.result);
172
+ const reportedTotal = Number(payload?.result_info?.total_pages);
173
+ totalPages = Number.isFinite(reportedTotal) && reportedTotal > 0 ? reportedTotal : page;
174
+ page += 1;
175
+ }
176
+ return domains;
177
+ }
178
+ function findCloudflarePagesDomain(input, projectName, domain) {
179
+ if (!domain) return null;
180
+ const env = cloudflarePagesEnv(input);
181
+ const accountId = String(env.CLOUDFLARE_ACCOUNT_ID ?? "").trim();
182
+ const token = String(env.CLOUDFLARE_API_TOKEN ?? "").trim();
183
+ if (accountId && token) {
184
+ const direct = cloudflareApiRequest(
185
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains/${encodeURIComponent(domain)}`,
186
+ { env, allowFailure: true }
187
+ )?.result ?? null;
188
+ if (cloudflarePagesDomainName(direct) === domain) return direct;
189
+ }
190
+ return listCloudflarePagesDomains(input, projectName).find((entry) => cloudflarePagesDomainName(entry) === domain) ?? null;
191
+ }
192
+ function cloudflareDnsRecordName(record) {
193
+ return typeof record?.name === "string" ? record.name : "";
194
+ }
195
+ function listCloudflareDnsRecords(input, recordName) {
196
+ const env = cloudflarePagesEnv(input);
197
+ const zoneId = recordName ? resolveCloudflareZoneIdForHost(input.graph.deployConfig, recordName, env) : null;
198
+ if (!zoneId || !recordName) return { zoneId, records: [] };
199
+ const payload = cloudflareApiRequest(
200
+ `/zones/${encodeURIComponent(zoneId)}/dns_records?name=${encodeURIComponent(recordName)}&per_page=100`,
201
+ { env, allowFailure: true }
202
+ );
203
+ return {
204
+ zoneId,
205
+ records: Array.isArray(payload?.result) ? payload.result : []
206
+ };
207
+ }
208
+ function ensureCloudflarePagesDns(input, projectName, branchName, domain) {
209
+ if (!domain || input.dryRun) return null;
210
+ const env = cloudflarePagesEnv(input);
211
+ const { zoneId, records } = listCloudflareDnsRecords(input, domain);
212
+ if (!zoneId) {
213
+ throw new Error(`Cloudflare DNS zone could not be resolved for Pages domain ${domain}.`);
214
+ }
215
+ const desired = {
216
+ type: "CNAME",
217
+ name: domain,
218
+ content: cloudflarePagesDnsTarget(projectName, branchName, input.environment),
219
+ proxied: true,
220
+ ttl: 1
221
+ };
222
+ const existing = records.find((entry) => cloudflareDnsRecordName(entry) === domain && String(entry?.type ?? "").toUpperCase() === "CNAME");
223
+ if (existing?.id) {
224
+ const unchanged = existing.content === desired.content && Boolean(existing.proxied) === desired.proxied;
225
+ if (unchanged) return existing;
226
+ return cloudflareApiRequest(
227
+ `/zones/${encodeURIComponent(zoneId)}/dns_records/${encodeURIComponent(existing.id)}`,
228
+ { method: "PATCH", body: desired, env }
229
+ )?.result ?? existing;
230
+ }
231
+ return cloudflareApiRequest(
232
+ `/zones/${encodeURIComponent(zoneId)}/dns_records`,
233
+ { method: "POST", body: desired, env }
234
+ )?.result ?? null;
235
+ }
236
+ function runCloudflarePagesBuild(input) {
237
+ const command = cloudflarePagesBuildCommand(input);
238
+ if (!command || input.dryRun) {
239
+ return;
240
+ }
241
+ const result = spawnSync("bash", ["-lc", command], {
242
+ cwd: input.graph.tenantRoot,
243
+ stdio: "inherit",
244
+ env: { ...process.env, ...cloudflarePagesEnv(input) },
245
+ encoding: "utf8"
246
+ });
247
+ if (result.status !== 0) {
248
+ throw new Error(`Cloudflare Pages build command failed: ${command}`);
249
+ }
250
+ }
251
+ function ensureCloudflarePagesProject(input, projectName) {
252
+ if (input.dryRun) return;
253
+ const productionBranch = cloudflarePagesBranchName({ ...input, environment: "prod" });
254
+ const result = runWrangler([
255
+ "pages",
256
+ "project",
257
+ "create",
258
+ projectName,
259
+ "--production-branch",
260
+ productionBranch
261
+ ], {
262
+ cwd: input.graph.tenantRoot,
263
+ capture: true,
264
+ allowFailure: true,
265
+ env: cloudflarePagesEnv(input)
266
+ });
267
+ if (result.status !== 0) {
268
+ const output = [result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join("\n");
269
+ if (!/already exists|code:\s*8000002/iu.test(output)) {
270
+ throw new Error(output || `Failed to create Cloudflare Pages project ${projectName}.`);
271
+ }
272
+ }
273
+ }
274
+ function ensureCloudflarePagesDomain(input, projectName, domain) {
275
+ const env = cloudflarePagesEnv(input);
276
+ const accountId = String(env.CLOUDFLARE_ACCOUNT_ID ?? "").trim();
277
+ if (!domain || !accountId || input.dryRun) return;
278
+ const existing = findCloudflarePagesDomain(input, projectName, domain);
279
+ if (existing) {
280
+ return;
281
+ }
282
+ const created = cloudflareApiRequest(
283
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains`,
284
+ {
285
+ method: "POST",
286
+ body: { name: domain },
287
+ allowFailure: true,
288
+ env
289
+ }
290
+ );
291
+ const errors = Array.isArray(created?.errors) ? created.errors.map((entry) => String(entry?.message ?? entry)).join("; ") : "";
292
+ if (errors && !/already exists|already added|already been taken|conflict/iu.test(errors)) {
293
+ throw new Error(`Failed to attach Cloudflare Pages custom domain ${domain}: ${errors}`);
294
+ }
295
+ const observed = findCloudflarePagesDomain(input, projectName, domain);
296
+ if (!observed) {
297
+ throw new Error(`Cloudflare Pages custom domain ${domain} was not attached to project ${projectName}.`);
298
+ }
299
+ }
300
+ function observeCloudflarePagesProject(input, projectName) {
301
+ const env = cloudflarePagesEnv(input);
302
+ const accountId = String(env.CLOUDFLARE_ACCOUNT_ID ?? "").trim();
303
+ const token = String(env.CLOUDFLARE_API_TOKEN ?? "").trim();
304
+ if (!accountId || !token) return null;
305
+ return cloudflareApiRequest(
306
+ `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}`,
307
+ { env, allowFailure: true }
308
+ )?.result ?? null;
309
+ }
310
+ function observeCloudflarePagesDomain(input, projectName, domain) {
311
+ return findCloudflarePagesDomain(input, projectName, domain);
312
+ }
313
+ function observeCloudflarePagesDeployment(input, projectName, branchName) {
314
+ const result = runWrangler(["pages", "deployment", "list", "--project-name", projectName, "--json"], {
315
+ cwd: input.graph.tenantRoot,
316
+ capture: true,
317
+ allowFailure: true,
318
+ env: cloudflarePagesEnv(input)
319
+ });
320
+ if (result.status !== 0) return null;
321
+ try {
322
+ const deployments = JSON.parse(result.stdout || "[]");
323
+ return (Array.isArray(deployments) ? deployments : []).find((entry) => entry?.Branch === branchName || entry?.branch === branchName) ?? null;
324
+ } catch {
325
+ return null;
326
+ }
327
+ }
328
+ function observeCloudflarePagesDns(input, projectName, branchName, domain) {
329
+ if (!domain) return null;
330
+ const expectedContent = cloudflarePagesDnsTarget(projectName, branchName, input.environment);
331
+ const { zoneId, records } = listCloudflareDnsRecords(input, domain);
332
+ const record = records.find(
333
+ (entry) => cloudflareDnsRecordName(entry) === domain && String(entry?.type ?? "").toUpperCase() === "CNAME" && entry?.content === expectedContent
334
+ ) ?? null;
335
+ return record ? { ...record, zoneId } : null;
336
+ }
337
+ function verifyCloudflarePagesPostconditions(input, projectName, domain) {
338
+ const branchName = cloudflarePagesBranchName(input);
339
+ const project = observeCloudflarePagesProject(input, projectName);
340
+ if (project?.name !== projectName) {
341
+ throw new Error(`Cloudflare Pages project ${projectName} was not observed after deploy.`);
342
+ }
343
+ const deployment = observeCloudflarePagesDeployment(input, projectName, branchName);
344
+ if (!deployment) {
345
+ throw new Error(`Cloudflare Pages project ${projectName} has no observed deployment for branch ${branchName}.`);
346
+ }
347
+ if (domain) {
348
+ const observedDomain = observeCloudflarePagesDomain(input, projectName, domain);
349
+ const observedName = cloudflarePagesDomainName(observedDomain);
350
+ if (observedName !== domain) {
351
+ throw new Error(`Cloudflare Pages custom domain ${domain} was not observed on project ${projectName} after deploy.`);
352
+ }
353
+ const dns = observeCloudflarePagesDns(input, projectName, branchName, domain);
354
+ if (!dns) {
355
+ throw new Error(`Cloudflare DNS record ${domain} -> ${cloudflarePagesDnsTarget(projectName, branchName, input.environment)} was not observed after deploy.`);
356
+ }
357
+ }
358
+ }
359
+ function deployCloudflarePages(input) {
360
+ const projectName = cloudflarePagesProjectName(input);
361
+ if (!projectName) {
362
+ return {
363
+ ...syntheticStatus(input),
364
+ status: "blocked",
365
+ warnings: ["Cloudflare Pages projectName is required for web-site deployment."]
366
+ };
367
+ }
368
+ const branchName = cloudflarePagesBranchName(input);
369
+ const buildOutputDir = cloudflarePagesBuildOutputDir(input);
370
+ const outputPath = resolve(input.graph.tenantRoot, buildOutputDir);
371
+ let observedProject = null;
372
+ let observedDomain = null;
373
+ let observedDns = null;
374
+ let observedDeployment = null;
375
+ if (!input.dryRun) {
376
+ runCloudflarePagesBuild(input);
377
+ if (!existsSync(outputPath)) {
378
+ throw new Error(`Cloudflare Pages build output does not exist: ${outputPath}`);
379
+ }
380
+ ensureCloudflarePagesProject(input, projectName);
381
+ ensureCloudflarePagesDomain(input, projectName, cloudflarePagesDomain(input));
382
+ ensureCloudflarePagesDns(input, projectName, branchName, cloudflarePagesDomain(input));
383
+ runWrangler([
384
+ "pages",
385
+ "deploy",
386
+ outputPath,
387
+ "--project-name",
388
+ projectName,
389
+ "--branch",
390
+ branchName
391
+ ], {
392
+ cwd: input.graph.tenantRoot,
393
+ capture: true,
394
+ env: cloudflarePagesEnv(input)
395
+ });
396
+ verifyCloudflarePagesPostconditions(input, projectName, cloudflarePagesDomain(input));
397
+ observedProject = observeCloudflarePagesProject(input, projectName);
398
+ observedDomain = observeCloudflarePagesDomain(input, projectName, cloudflarePagesDomain(input));
399
+ observedDns = observeCloudflarePagesDns(input, projectName, branchName, cloudflarePagesDomain(input));
400
+ observedDeployment = observeCloudflarePagesDeployment(input, projectName, branchName);
401
+ }
402
+ return {
403
+ status: input.dryRun ? "pending" : "ready",
404
+ locators: {
405
+ hostId: input.unit.host.id,
406
+ projectGroupId: input.unit.projectGroup?.id ?? null,
407
+ projectName,
408
+ branchName,
409
+ domain: cloudflarePagesDomain(input),
410
+ pagesDevUrl: cloudflarePagesDeploymentUrl(projectName, branchName, input.environment)
411
+ },
412
+ state: {
413
+ unitId: input.unit.id,
414
+ serviceType: input.unit.serviceType.id,
415
+ placement: input.unit.placement,
416
+ projectName,
417
+ branchName,
418
+ buildOutputDir,
419
+ buildCommand: cloudflarePagesBuildCommand(input),
420
+ observedProjectName: observedProject?.name ?? (input.dryRun ? null : projectName),
421
+ observedDomain: cloudflarePagesDomainName(observedDomain),
422
+ observedDnsRecord: observedDns ? {
423
+ name: observedDns.name ?? null,
424
+ type: observedDns.type ?? null,
425
+ content: observedDns.content ?? null,
426
+ proxied: observedDns.proxied ?? null
427
+ } : null,
428
+ observedDeployment: observedDeployment ? {
429
+ id: observedDeployment.Id ?? observedDeployment.id ?? null,
430
+ branch: observedDeployment.Branch ?? observedDeployment.branch ?? null,
431
+ deployment: observedDeployment.Deployment ?? observedDeployment.url ?? null
432
+ } : null,
433
+ dryRun: input.dryRun === true,
434
+ applied: input.dryRun !== true
435
+ },
436
+ warnings: []
437
+ };
438
+ }
439
+ function createCloudflareHostAdapter() {
440
+ const base = createSyntheticHostAdapter("cloudflare", "Cloudflare", [
441
+ "web-site",
442
+ "object-store",
443
+ "database",
444
+ "dns",
445
+ "domain",
446
+ "secret",
447
+ "variable",
448
+ "deployment",
449
+ "health"
450
+ ], PROVIDER_ENVIRONMENTS);
451
+ const isPagesSite = (input) => input.unit.serviceType.id === "web-site";
452
+ return {
453
+ ...base,
454
+ refresh(input) {
455
+ if (!isPagesSite(input)) return base.refresh(input);
456
+ const projectName = cloudflarePagesProjectName(input);
457
+ const project = projectName ? observeCloudflarePagesProject(input, projectName) : null;
458
+ const domain = cloudflarePagesDomain(input);
459
+ const branchName = cloudflarePagesBranchName(input);
460
+ const observedDomain = projectName ? observeCloudflarePagesDomain(input, projectName, domain) : null;
461
+ const observedDns = projectName ? observeCloudflarePagesDns(input, projectName, branchName, domain) : null;
462
+ const observedDeployment = projectName ? observeCloudflarePagesDeployment(input, projectName, branchName) : null;
463
+ const domainReady = !domain || Boolean(observedDomain && observedDns);
464
+ const deploymentReady = Boolean(observedDeployment);
465
+ return {
466
+ status: projectName && project?.name === projectName && domainReady && deploymentReady ? "ready" : projectName ? "pending" : "blocked",
467
+ locators: {
468
+ hostId: input.unit.host.id,
469
+ projectGroupId: input.unit.projectGroup?.id ?? null,
470
+ projectName,
471
+ domain,
472
+ pagesDevUrl: projectName ? cloudflarePagesDeploymentUrl(projectName, branchName, input.environment) : null
473
+ },
474
+ state: {
475
+ unitId: input.unit.id,
476
+ serviceType: input.unit.serviceType.id,
477
+ placement: input.unit.placement,
478
+ projectName,
479
+ branchName,
480
+ observedProjectName: project?.name ?? null,
481
+ observedDomain: cloudflarePagesDomainName(observedDomain),
482
+ observedDnsRecord: observedDns ? {
483
+ name: observedDns.name ?? null,
484
+ type: observedDns.type ?? null,
485
+ content: observedDns.content ?? null,
486
+ proxied: observedDns.proxied ?? null
487
+ } : null,
488
+ observedDeployment: observedDeployment ? {
489
+ id: observedDeployment.Id ?? observedDeployment.id ?? null,
490
+ branch: observedDeployment.Branch ?? observedDeployment.branch ?? null,
491
+ deployment: observedDeployment.Deployment ?? observedDeployment.url ?? null
492
+ } : null,
493
+ buildOutputDir: cloudflarePagesBuildOutputDir(input),
494
+ buildCommand: cloudflarePagesBuildCommand(input)
495
+ },
496
+ warnings: projectName ? [] : ["Cloudflare Pages projectName is missing."]
497
+ };
498
+ },
499
+ apply(input) {
500
+ if (!isPagesSite(input)) return base.apply(input);
501
+ return deployCloudflarePages(input);
502
+ },
503
+ verify(input) {
504
+ if (!isPagesSite(input)) return base.verify(input);
505
+ if (input.dryRun) return base.verify(input);
506
+ const projectName = cloudflarePagesProjectName(input);
507
+ const branchName = projectName ? cloudflarePagesBranchName(input) : null;
508
+ const domain = cloudflarePagesDomain(input);
509
+ const observed = input.observed;
510
+ const observedState = observed.state && typeof observed.state === "object" ? observed.state : {};
511
+ const observedProjectName = observedState.observedProjectName ?? observedState.projectName ?? null;
512
+ const observedDomain = observedState.observedDomain ?? null;
513
+ const observedDnsRecord = observedState.observedDnsRecord ?? null;
514
+ const observedDeployment = observedState.observedDeployment ?? null;
515
+ const checks = [
516
+ {
517
+ key: "pages-project.exists",
518
+ label: "Cloudflare Pages project exists",
519
+ ok: Boolean(projectName && observedProjectName === projectName),
520
+ expected: projectName,
521
+ observed: observedProjectName,
522
+ issues: projectName && observedProjectName === projectName ? [] : [`Cloudflare Pages project ${projectName ?? "(unset)"} was not observed.`]
523
+ },
524
+ {
525
+ key: "pages-deployment.exists",
526
+ label: "Cloudflare Pages branch deployment exists",
527
+ ok: Boolean(observedDeployment),
528
+ expected: branchName,
529
+ observed: observedDeployment,
530
+ issues: observedDeployment ? [] : [`Cloudflare Pages project ${projectName ?? "(unset)"} has no deployment for branch ${branchName ?? "(unset)"}.`]
531
+ },
532
+ {
533
+ key: "pages-domain.exists",
534
+ label: "Cloudflare Pages custom domain is attached",
535
+ ok: !domain || observedDomain === domain,
536
+ expected: domain,
537
+ observed: observedDomain,
538
+ issues: !domain || observedDomain === domain ? [] : [`Cloudflare Pages custom domain ${domain} is not attached to project ${projectName ?? "(unset)"}.`]
539
+ },
540
+ {
541
+ key: "pages-dns.exists",
542
+ label: "Cloudflare DNS record points to the Pages branch",
543
+ ok: !domain || Boolean(observedDnsRecord),
544
+ expected: domain && projectName && branchName ? {
545
+ name: domain,
546
+ type: "CNAME",
547
+ content: cloudflarePagesDnsTarget(projectName, branchName, input.environment),
548
+ proxied: true
549
+ } : null,
550
+ observed: observedDnsRecord,
551
+ issues: !domain || observedDnsRecord ? [] : [`Cloudflare DNS record ${domain} -> ${projectName && branchName ? cloudflarePagesDnsTarget(projectName, branchName, input.environment) : "(unset)"} is missing.`]
552
+ }
553
+ ];
554
+ return {
555
+ unitId: input.unit.id,
556
+ status: checks.every((check) => check.ok) ? "ready" : "pending",
557
+ verified: checks.every((check) => check.ok),
558
+ checks,
559
+ warnings: []
560
+ };
561
+ },
562
+ status(input) {
563
+ return isPagesSite(input) ? this.refresh(input) : base.status(input);
564
+ }
565
+ };
566
+ }
567
+ function createDefaultHostAdapters() {
568
+ return {
569
+ railway: createSyntheticHostAdapter("railway", "Railway", [
570
+ "project",
571
+ "environment",
572
+ "container",
573
+ "volume",
574
+ "database",
575
+ "domain",
576
+ "secret",
577
+ "variable",
578
+ "deployment",
579
+ "scheduled-job",
580
+ "health",
581
+ "logs"
582
+ ], PROVIDER_ENVIRONMENTS),
583
+ cloudflare: createCloudflareHostAdapter(),
584
+ github: createSyntheticHostAdapter("github", "GitHub", [
585
+ "source-repository",
586
+ "workflow",
587
+ "secret",
588
+ "variable",
589
+ "health"
590
+ ], PROVIDER_ENVIRONMENTS),
591
+ smtp: createSyntheticHostAdapter("smtp", "SMTP", [
592
+ "email-relay",
593
+ "secret",
594
+ "health"
595
+ ], ALL_ENVIRONMENTS),
596
+ "local-process": createSyntheticHostAdapter("local-process", "Local process", [
597
+ "process",
598
+ "web-site",
599
+ "container",
600
+ "variable",
601
+ "deployment",
602
+ "health",
603
+ "logs",
604
+ "port",
605
+ "hot-reload"
606
+ ], ["local"]),
607
+ "local-docker": createSyntheticHostAdapter("local-docker", "Local Docker", [
608
+ "container",
609
+ "volume",
610
+ "database",
611
+ "object-store",
612
+ "secret",
613
+ "variable",
614
+ "deployment",
615
+ "health",
616
+ "logs"
617
+ ], ["local"])
618
+ };
619
+ }
620
+ function serviceType(id, label, placement, requiredCapabilities, defaultHostByEnvironment, composes = []) {
621
+ return {
622
+ id,
623
+ label,
624
+ placement,
625
+ requiredCapabilities,
626
+ defaultHostByEnvironment,
627
+ composes,
628
+ describe(unit) {
629
+ return `${label} on ${unit.host.label}`;
630
+ }
631
+ };
632
+ }
633
+ function createDefaultServiceTypeAdapters() {
634
+ return {
635
+ "web-site": serviceType("web-site", "Web site", "web", ["web-site", "deployment", "health"], {
636
+ local: "local-process",
637
+ staging: "cloudflare",
638
+ prod: "cloudflare"
639
+ }),
640
+ "container-api": serviceType("container-api", "Container API", "api", ["container", "variable", "deployment", "health"], {
641
+ local: "local-process",
642
+ staging: "railway",
643
+ prod: "railway"
644
+ }),
645
+ "stateful-container": serviceType("stateful-container", "Stateful container", "operations", ["container", "volume", "variable", "deployment", "health"], {
646
+ local: "local-docker",
647
+ staging: "railway",
648
+ prod: "railway"
649
+ }),
650
+ "runner-pool": serviceType("runner-pool", "Runner pool", "runner-capacity", ["container", "volume", "variable", "deployment", "health"], {
651
+ local: "local-docker",
652
+ staging: "railway",
653
+ prod: "railway"
654
+ }),
655
+ "scheduled-job": serviceType("scheduled-job", "Scheduled job", "operations", ["scheduled-job", "variable", "deployment", "health"], {
656
+ local: "local-process",
657
+ staging: "railway",
658
+ prod: "railway"
659
+ }),
660
+ "relational-database": serviceType("relational-database", "Relational database", "database", ["database", "secret", "health"], {
661
+ local: "local-docker",
662
+ staging: "railway",
663
+ prod: "railway"
664
+ }),
665
+ "object-store": serviceType("object-store", "Object store", "content-storage", ["object-store", "health"], {
666
+ local: "local-docker",
667
+ staging: "cloudflare",
668
+ prod: "cloudflare"
669
+ }),
670
+ "source-repository": serviceType("source-repository", "Source repository", "repository", ["source-repository", "health"], {
671
+ local: "local-process",
672
+ staging: "github",
673
+ prod: "github"
674
+ }),
675
+ "email-relay": serviceType("email-relay", "Email relay", "email", ["email-relay", "secret", "health"], {
676
+ local: "smtp",
677
+ staging: "smtp",
678
+ prod: "smtp"
679
+ }),
680
+ "knowledge-library": serviceType("knowledge-library", "Knowledge library", "knowledge-library", [], {
681
+ local: "local-docker",
682
+ staging: "railway",
683
+ prod: "railway"
684
+ }, ["treedx-federation"]),
685
+ "treedx-node": serviceType("treedx-node", "TreeDX node", "knowledge-library", ["container", "volume", "variable", "deployment", "health"], {
686
+ local: "local-docker",
687
+ staging: "railway",
688
+ prod: "railway"
689
+ }, ["stateful-container"]),
690
+ "treedx-federation": serviceType("treedx-federation", "TreeDX federation", "knowledge-library", [], {
691
+ local: "local-docker",
692
+ staging: "railway",
693
+ prod: "railway"
694
+ }, ["treedx-node"]),
695
+ "treeseed-control-plane": serviceType("treeseed-control-plane", "Treeseed control plane", "operations", [], {
696
+ local: "local-process",
697
+ staging: "railway",
698
+ prod: "railway"
699
+ }, ["container-api", "runner-pool", "relational-database"]),
700
+ "capacity-provider": serviceType("capacity-provider", "Capacity provider", "runner-capacity", [], {
701
+ local: "local-docker",
702
+ staging: "railway",
703
+ prod: "railway"
704
+ }, ["runner-pool"])
705
+ };
706
+ }
707
+ function createDefaultHostingProfiles() {
708
+ return [
709
+ {
710
+ id: "treeseed-managed-public-team",
711
+ label: "TreeSeed managed public team",
712
+ description: "Public teams use the shared public TreeDX federation and managed web/content defaults.",
713
+ services: [],
714
+ metadata: { publicRead: true, managed: true }
715
+ },
716
+ {
717
+ id: "treeseed-managed-private-team",
718
+ label: "TreeSeed managed private team",
719
+ description: "Private teams receive dedicated managed infrastructure for privacy-bearing data.",
720
+ services: [],
721
+ metadata: { publicRead: false, managed: true }
722
+ },
723
+ {
724
+ id: "customer-self-hosted",
725
+ label: "Customer self-hosted",
726
+ description: "Customer-owned hosts satisfy the same service capabilities.",
727
+ services: [],
728
+ metadata: { managed: false }
729
+ },
730
+ {
731
+ id: "local-development",
732
+ label: "Local development",
733
+ description: "Hot-reload local processes for code services and local Docker for stateful services.",
734
+ services: [],
735
+ metadata: { local: true, hotReload: true }
736
+ },
737
+ {
738
+ id: "production-like-local",
739
+ label: "Production-like local",
740
+ description: "Local containers model provider-backed runtime behavior without mutating hosted resources.",
741
+ services: [],
742
+ metadata: { local: true, productionLike: true }
743
+ }
744
+ ];
745
+ }
746
+ function sanitizedUnitConfig(unit) {
747
+ return {
748
+ id: unit.id,
749
+ label: unit.label,
750
+ serviceType: unit.serviceType.id,
751
+ placement: unit.placement,
752
+ hostId: unit.host.id,
753
+ environment: unit.environment,
754
+ projectGroupId: unit.projectGroup?.id ?? null,
755
+ requiredCapabilities: unit.requiredCapabilities,
756
+ secretRefs: unit.secretRefs,
757
+ variableRefs: unit.variableRefs,
758
+ application: unit.application ? {
759
+ id: unit.application.id,
760
+ relativeRoot: unit.application.relativeRoot,
761
+ roles: unit.application.roles
762
+ } : null,
763
+ config: redactSensitiveConfig(unit.config),
764
+ metadata: redactSensitiveConfig(unit.metadata)
765
+ };
766
+ }
767
+ function redactSensitiveConfig(value) {
768
+ if (Array.isArray(value)) return value.map((entry) => redactSensitiveConfig(entry));
769
+ if (!value || typeof value !== "object") return value;
770
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => {
771
+ if (/(secret|token|password|privateKey|apiKey|credential)/iu.test(key)) {
772
+ return [key, "[redacted]"];
773
+ }
774
+ return [key, redactSensitiveConfig(entry)];
775
+ }));
776
+ }
777
+ function summarizePlacementStatus(statuses) {
778
+ if (statuses.includes("blocked")) return "blocked";
779
+ if (statuses.includes("degraded")) return "degraded";
780
+ if (statuses.includes("pending")) return "pending";
781
+ if (statuses.includes("ready")) return "ready";
782
+ return "unknown";
783
+ }
784
+ export {
785
+ createDefaultHostAdapters,
786
+ createDefaultHostingProfiles,
787
+ createDefaultServiceTypeAdapters,
788
+ redactSensitiveConfig,
789
+ sanitizedUnitConfig,
790
+ summarizePlacementStatus
791
+ };