@treeseed/sdk 0.4.8 → 0.4.10

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/control-plane-client.d.ts +45 -0
  3. package/dist/control-plane-client.js +229 -0
  4. package/dist/control-plane.d.ts +94 -0
  5. package/dist/control-plane.js +125 -0
  6. package/dist/d1-store.d.ts +56 -1
  7. package/dist/d1-store.js +132 -0
  8. package/dist/dispatch.d.ts +4 -0
  9. package/dist/dispatch.js +180 -0
  10. package/dist/index.d.ts +14 -2
  11. package/dist/index.js +94 -4
  12. package/dist/operations/services/config-runtime.d.ts +10 -0
  13. package/dist/operations/services/config-runtime.js +62 -4
  14. package/dist/operations/services/deploy.d.ts +95 -3
  15. package/dist/operations/services/deploy.js +351 -10
  16. package/dist/operations/services/github-automation.d.ts +37 -1
  17. package/dist/operations/services/github-automation.js +71 -14
  18. package/dist/operations/services/project-platform.d.ts +835 -0
  19. package/dist/operations/services/project-platform.js +782 -0
  20. package/dist/operations/services/railway-deploy.d.ts +113 -18
  21. package/dist/operations/services/railway-deploy.js +357 -8
  22. package/dist/operations/services/runtime-tools.d.ts +25 -1
  23. package/dist/operations/services/runtime-tools.js +66 -5
  24. package/dist/operations/services/template-registry.d.ts +1 -1
  25. package/dist/operations/services/template-registry.js +17 -3
  26. package/dist/platform/books-data.d.ts +3 -4
  27. package/dist/platform/books-data.js +30 -4
  28. package/dist/platform/contracts.d.ts +56 -4
  29. package/dist/platform/deploy-config.js +109 -4
  30. package/dist/platform/deploy-runtime.d.ts +2 -0
  31. package/dist/platform/deploy-runtime.js +9 -1
  32. package/dist/platform/env.yaml +677 -0
  33. package/dist/platform/environment.js +57 -2
  34. package/dist/platform/plugin.d.ts +8 -0
  35. package/dist/platform/plugins/constants.d.ts +2 -0
  36. package/dist/platform/plugins/constants.js +2 -0
  37. package/dist/platform/plugins/runtime.d.ts +2 -0
  38. package/dist/platform/plugins/runtime.js +9 -1
  39. package/dist/platform/plugins.d.ts +1 -1
  40. package/dist/platform/plugins.js +4 -0
  41. package/dist/platform/published-content-pipeline.d.ts +84 -0
  42. package/dist/platform/published-content-pipeline.js +543 -0
  43. package/dist/platform/published-content.d.ts +223 -0
  44. package/dist/platform/published-content.js +588 -0
  45. package/dist/platform/tenant/runtime-config.d.ts +1 -1
  46. package/dist/platform/tenant/runtime-config.js +34 -1
  47. package/dist/platform/tenant-config.d.ts +2 -1
  48. package/dist/platform/tenant-config.js +17 -1
  49. package/dist/platform/utils/site-config-schema.js +104 -0
  50. package/dist/plugin-default.d.ts +2 -0
  51. package/dist/plugin-default.js +2 -0
  52. package/dist/remote.d.ts +65 -9
  53. package/dist/remote.js +104 -28
  54. package/dist/scripts/check-build-warnings.js +50 -0
  55. package/dist/scripts/config-treeseed.js +7 -0
  56. package/dist/scripts/tenant-workflow-action.js +71 -0
  57. package/dist/sdk-dispatch.d.ts +12 -0
  58. package/dist/sdk-dispatch.js +142 -0
  59. package/dist/sdk-types.d.ts +579 -7
  60. package/dist/sdk-types.js +53 -1
  61. package/dist/sdk.d.ts +17 -1
  62. package/dist/sdk.js +109 -0
  63. package/dist/stores/operational-store.d.ts +22 -2
  64. package/dist/stores/operational-store.js +235 -0
  65. package/dist/template-catalog.js +8 -1
  66. package/dist/treeseed/template-catalog/templates/starter-basic/template/treeseed.site.yaml +20 -0
  67. package/dist/types/cloudflare.d.ts +23 -0
  68. package/dist/workflow/operations.d.ts +12 -3
  69. package/dist/workflow/policy.d.ts +1 -1
  70. package/dist/workflow-state.js +2 -1
  71. package/package.json +7 -2
  72. package/templates/github/deploy.workflow.yml +442 -0
  73. package/templates/github/hosted-project.workflow.yml +77 -0
@@ -10,7 +10,7 @@ const DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
10
10
  const GENERATED_ROOT = ".treeseed/generated";
11
11
  const STATE_ROOT = ".treeseed/state";
12
12
  const PERSISTENT_SCOPES = /* @__PURE__ */ new Set(["local", "staging", "prod"]);
13
- const MANAGED_SERVICE_KEYS = ["api", "agents", "gateway", "manager", "worker", "workdayStart", "workdayReport"];
13
+ const MANAGED_SERVICE_KEYS = ["api", "agents", "manager", "worker", "runner", "workdayStart", "workdayReport"];
14
14
  const TRESEED_ENVELOPE_SCHEMA_GENERATION = "runtime-envelopes-v1";
15
15
  const TRESEED_MIGRATION_WAVE_ID = "0005_runtime_envelopes";
16
16
  const TRESEED_SUPPORTED_PAYLOAD_RANGE = { min: 1, max: 1 };
@@ -128,17 +128,57 @@ function relativeFromGeneratedRoot(targetPath, generatedRoot) {
128
128
  return relative(generatedRoot, targetPath).replaceAll("\\", "/");
129
129
  }
130
130
  function buildPublicVars(deployConfig) {
131
+ const contentRuntimeProvider = deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay";
132
+ const contentPublishProvider = deployConfig.providers?.content?.publish ?? contentRuntimeProvider;
133
+ const contentDefaultTeamId = deployConfig.hosting?.teamId ?? deployConfig.slug;
134
+ const contentManifestKeyTemplate = deployConfig.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json";
135
+ const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
136
+ const contentManifestKey = contentManifestKeyTemplate.replaceAll("{teamId}", contentDefaultTeamId);
137
+ const hostedProject = (deployConfig.hosting?.kind ?? "self_hosted_project") === "hosted_project";
138
+ const workerRailway = deployConfig.services?.worker?.railway ?? {};
131
139
  return {
140
+ TREESEED_HOSTING_KIND: deployConfig.hosting?.kind ?? "self_hosted_project",
141
+ TREESEED_HOSTING_REGISTRATION: deployConfig.hosting?.registration ?? "none",
142
+ TREESEED_MARKET_API_BASE_URL: deployConfig.hosting?.marketBaseUrl ?? "",
143
+ TREESEED_HOSTING_TEAM_ID: deployConfig.hosting?.teamId ?? contentDefaultTeamId,
144
+ TREESEED_PROJECT_ID: deployConfig.hosting?.projectId ?? deployConfig.slug,
132
145
  TREESEED_AGENT_EXECUTION_PROVIDER: deployConfig.providers?.agents?.execution ?? "stub",
133
146
  TREESEED_AGENT_REPOSITORY_PROVIDER: deployConfig.providers?.agents?.repository ?? "stub",
134
147
  TREESEED_AGENT_VERIFICATION_PROVIDER: deployConfig.providers?.agents?.verification ?? "stub",
148
+ TREESEED_CONTENT_RUNTIME_PROVIDER: contentRuntimeProvider,
149
+ TREESEED_CONTENT_PUBLISH_PROVIDER: contentPublishProvider,
150
+ TREESEED_CONTENT_DEFAULT_TEAM_ID: contentDefaultTeamId,
151
+ TREESEED_CONTENT_MANIFEST_KEY: contentManifestKey,
152
+ TREESEED_CONTENT_MANIFEST_KEY_TEMPLATE: contentManifestKeyTemplate,
153
+ TREESEED_CONTENT_PREVIEW_ROOT_TEMPLATE: contentPreviewRootTemplate,
154
+ TREESEED_EDITORIAL_PREVIEW_ROOT: contentPreviewRootTemplate.replaceAll("{teamId}", contentDefaultTeamId),
155
+ TREESEED_EDITORIAL_PREVIEW_TTL_HOURS: String(deployConfig.cloudflare.r2?.previewTtlHours ?? 168),
156
+ TREESEED_CONTENT_BUCKET_NAME: deployConfig.cloudflare.r2?.bucketName ?? "",
157
+ TREESEED_CONTENT_PUBLIC_BASE_URL: deployConfig.cloudflare.r2?.publicBaseUrl ?? "",
158
+ TREESEED_WORKER_POOL_SCALER: envOrNull("TREESEED_WORKER_POOL_SCALER") ?? (hostedProject ? "railway" : ""),
159
+ TREESEED_WORKDAY_TIMEZONE: envOrNull("TREESEED_WORKDAY_TIMEZONE") ?? "",
160
+ TREESEED_WORKDAY_WINDOWS_JSON: envOrNull("TREESEED_WORKDAY_WINDOWS_JSON") ?? "",
161
+ TREESEED_WORKDAY_TASK_CREDIT_BUDGET: envOrNull("TREESEED_WORKDAY_TASK_CREDIT_BUDGET") ?? "",
162
+ TREESEED_MANAGER_MAX_QUEUED_TASKS: envOrNull("TREESEED_MANAGER_MAX_QUEUED_TASKS") ?? "",
163
+ TREESEED_MANAGER_MAX_QUEUED_CREDITS: envOrNull("TREESEED_MANAGER_MAX_QUEUED_CREDITS") ?? "",
164
+ TREESEED_MANAGER_PRIORITY_MODELS: envOrNull("TREESEED_MANAGER_PRIORITY_MODELS") ?? "",
165
+ TREESEED_TASK_CREDIT_WEIGHTS_JSON: envOrNull("TREESEED_TASK_CREDIT_WEIGHTS_JSON") ?? "",
166
+ TREESEED_AGENT_POOL_MIN_WORKERS: envOrNull("TREESEED_AGENT_POOL_MIN_WORKERS") ?? "",
167
+ TREESEED_AGENT_POOL_MAX_WORKERS: envOrNull("TREESEED_AGENT_POOL_MAX_WORKERS") ?? "",
168
+ TREESEED_AGENT_POOL_TARGET_QUEUE_DEPTH: envOrNull("TREESEED_AGENT_POOL_TARGET_QUEUE_DEPTH") ?? "",
169
+ TREESEED_AGENT_POOL_COOLDOWN_SECONDS: envOrNull("TREESEED_AGENT_POOL_COOLDOWN_SECONDS") ?? "",
170
+ TREESEED_RAILWAY_PROJECT_ID: envOrNull("TREESEED_RAILWAY_PROJECT_ID") ?? workerRailway.projectId ?? "",
171
+ TREESEED_RAILWAY_ENVIRONMENT_ID: envOrNull("TREESEED_RAILWAY_ENVIRONMENT_ID") ?? "",
172
+ TREESEED_RAILWAY_WORKER_SERVICE_ID: envOrNull("TREESEED_RAILWAY_WORKER_SERVICE_ID") ?? workerRailway.serviceId ?? "",
135
173
  TREESEED_PUBLIC_TURNSTILE_SITE_KEY: envOrNull("TREESEED_PUBLIC_TURNSTILE_SITE_KEY") ?? ""
136
174
  };
137
175
  }
138
176
  function buildSecretMap(deployConfig, state) {
139
177
  const generatedSecret = state.generatedSecrets?.TREESEED_FORM_TOKEN_SECRET ?? randomBytes(24).toString("hex");
178
+ const previewSecret = state.generatedSecrets?.TREESEED_EDITORIAL_PREVIEW_SECRET ?? randomBytes(24).toString("hex");
140
179
  return {
141
180
  TREESEED_FORM_TOKEN_SECRET: envOrNull("TREESEED_FORM_TOKEN_SECRET") ?? generatedSecret,
181
+ TREESEED_EDITORIAL_PREVIEW_SECRET: envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET") ?? previewSecret,
142
182
  TREESEED_TURNSTILE_SECRET_KEY: envOrNull("TREESEED_TURNSTILE_SECRET_KEY"),
143
183
  TREESEED_SMTP_HOST: deployConfig.smtp?.enabled ? envOrNull("TREESEED_SMTP_HOST") : null,
144
184
  TREESEED_SMTP_PORT: deployConfig.smtp?.enabled ? envOrNull("TREESEED_SMTP_PORT") : null,
@@ -151,6 +191,10 @@ function buildSecretMap(deployConfig, state) {
151
191
  function defaultStateFromConfig(deployConfig, target) {
152
192
  const workerName = targetWorkerName(deployConfig, target);
153
193
  const suffix = target.kind === "persistent" ? target.scope : sanitizeSegment(target.branchName);
194
+ const contentManifestKeyTemplate = deployConfig.cloudflare.r2?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json";
195
+ const contentPreviewRootTemplate = deployConfig.cloudflare.r2?.previewRootTemplate ?? "teams/{teamId}/previews";
196
+ const contentDefaultTeamId = deployConfig.hosting?.teamId ?? deployConfig.slug;
197
+ const contentManifestKey = contentManifestKeyTemplate.replaceAll("{teamId}", contentDefaultTeamId);
154
198
  return {
155
199
  version: 2,
156
200
  target,
@@ -179,15 +223,49 @@ function defaultStateFromConfig(deployConfig, target) {
179
223
  agentWork: {
180
224
  name: deployConfig.cloudflare.queueName ?? "agent-work",
181
225
  dlqName: deployConfig.cloudflare.dlqName ?? "agent-work-dlq",
182
- binding: deployConfig.cloudflare.queueBinding ?? "AGENT_WORK_QUEUE"
226
+ binding: deployConfig.cloudflare.queueBinding ?? "AGENT_WORK_QUEUE",
227
+ queueId: null,
228
+ dlqId: null
183
229
  }
184
230
  },
231
+ pages: {
232
+ projectName: target.kind === "persistent" ? target.scope === "prod" ? deployConfig.cloudflare.pages?.projectName ?? deployConfig.slug : deployConfig.cloudflare.pages?.previewProjectName ?? `${deployConfig.cloudflare.pages?.projectName ?? deployConfig.slug}-staging` : deployConfig.cloudflare.pages?.previewProjectName ?? `${deployConfig.cloudflare.pages?.projectName ?? deployConfig.slug}-preview`,
233
+ productionBranch: deployConfig.cloudflare.pages?.productionBranch ?? "main",
234
+ stagingBranch: deployConfig.cloudflare.pages?.stagingBranch ?? "staging",
235
+ buildOutputDir: deployConfig.cloudflare.pages?.buildOutputDir ?? "dist",
236
+ url: null
237
+ },
238
+ content: {
239
+ runtimeProvider: deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay",
240
+ publishProvider: deployConfig.providers?.content?.publish ?? deployConfig.providers?.content?.runtime ?? "team_scoped_r2_overlay",
241
+ defaultTeamId: contentDefaultTeamId,
242
+ r2Binding: deployConfig.cloudflare.r2?.binding ?? null,
243
+ bucketName: deployConfig.cloudflare.r2?.bucketName ?? null,
244
+ publicBaseUrl: deployConfig.cloudflare.r2?.publicBaseUrl ?? null,
245
+ manifestKeyTemplate: contentManifestKeyTemplate,
246
+ previewRootTemplate: contentPreviewRootTemplate,
247
+ previewTtlHours: deployConfig.cloudflare.r2?.previewTtlHours ?? 168,
248
+ manifestKey: contentManifestKey,
249
+ lastPublishedManifestRevision: null,
250
+ lastPublishedManifestSha256: null
251
+ },
252
+ hosting: {
253
+ kind: deployConfig.hosting?.kind ?? "self_hosted_project",
254
+ registration: deployConfig.hosting?.registration ?? "none",
255
+ marketBaseUrl: deployConfig.hosting?.marketBaseUrl ?? null,
256
+ teamId: deployConfig.hosting?.teamId ?? contentDefaultTeamId,
257
+ projectId: deployConfig.hosting?.projectId ?? deployConfig.slug
258
+ },
185
259
  generatedSecrets: {},
186
260
  readiness: {
261
+ configured: false,
262
+ provisioned: false,
263
+ deployable: false,
187
264
  initialized: false,
188
265
  initializedAt: null,
189
266
  lastValidatedAt: null,
190
- lastConfigFingerprint: null
267
+ lastConfigFingerprint: null,
268
+ lastValidationSummary: null
191
269
  },
192
270
  lastDeployedUrl: target.kind === "branch" ? targetWorkersDevUrl(workerName) : null,
193
271
  lastManifestFingerprint: null,
@@ -210,15 +288,18 @@ function defaultStateFromConfig(deployConfig, target) {
210
288
  workerName: serviceConfig?.cloudflare?.workerName ?? null,
211
289
  rootDir: serviceConfig?.railway?.rootDir ?? serviceConfig?.rootDir ?? null,
212
290
  environment: serviceConfig?.environments?.[scope]?.railwayEnvironment ?? scope,
291
+ schedule: serviceConfig?.railway?.schedule ?? null,
213
292
  publicBaseUrl: baseUrl,
214
293
  initialized: false,
215
294
  lastDeploymentTimestamp: null,
216
295
  lastDeployedUrl: baseUrl,
217
- lastDeploymentCommand: null
296
+ lastDeploymentCommand: null,
297
+ lastScheduleSyncAt: null
218
298
  }
219
299
  ];
220
300
  })
221
301
  ),
302
+ railwaySchedules: {},
222
303
  runtimeCompatibility: {
223
304
  envelopeSchemaGeneration: TRESEED_ENVELOPE_SCHEMA_GENERATION,
224
305
  migrationWaveId: TRESEED_MIGRATION_WAVE_ID,
@@ -270,13 +351,49 @@ function loadDeployState(tenantRoot, deployConfig, options = {}) {
270
351
  ...persisted.queues?.agentWork ?? {},
271
352
  name: defaults.queues?.agentWork?.name ?? persisted.queues?.agentWork?.name ?? "agent-work",
272
353
  dlqName: defaults.queues?.agentWork?.dlqName ?? persisted.queues?.agentWork?.dlqName ?? "agent-work-dlq",
273
- binding: defaults.queues?.agentWork?.binding ?? persisted.queues?.agentWork?.binding ?? "AGENT_WORK_QUEUE"
354
+ binding: defaults.queues?.agentWork?.binding ?? persisted.queues?.agentWork?.binding ?? "AGENT_WORK_QUEUE",
355
+ queueId: persisted.queues?.agentWork?.queueId ?? defaults.queues?.agentWork?.queueId ?? null,
356
+ dlqId: persisted.queues?.agentWork?.dlqId ?? defaults.queues?.agentWork?.dlqId ?? null
274
357
  }
275
358
  },
276
359
  generatedSecrets: {
277
360
  ...defaults.generatedSecrets ?? {},
278
361
  ...persisted.generatedSecrets ?? {}
279
362
  },
363
+ content: {
364
+ ...defaults.content ?? {},
365
+ ...persisted.content ?? {},
366
+ runtimeProvider: defaults.content?.runtimeProvider ?? persisted.content?.runtimeProvider ?? "team_scoped_r2_overlay",
367
+ publishProvider: defaults.content?.publishProvider ?? persisted.content?.publishProvider ?? "team_scoped_r2_overlay",
368
+ defaultTeamId: defaults.content?.defaultTeamId ?? persisted.content?.defaultTeamId ?? deployConfig.slug,
369
+ r2Binding: defaults.content?.r2Binding ?? persisted.content?.r2Binding ?? null,
370
+ bucketName: defaults.content?.bucketName ?? persisted.content?.bucketName ?? null,
371
+ publicBaseUrl: defaults.content?.publicBaseUrl ?? persisted.content?.publicBaseUrl ?? null,
372
+ manifestKeyTemplate: defaults.content?.manifestKeyTemplate ?? persisted.content?.manifestKeyTemplate ?? "teams/{teamId}/published/common.json",
373
+ previewRootTemplate: defaults.content?.previewRootTemplate ?? persisted.content?.previewRootTemplate ?? "teams/{teamId}/previews",
374
+ previewTtlHours: defaults.content?.previewTtlHours ?? persisted.content?.previewTtlHours ?? 168,
375
+ manifestKey: defaults.content?.manifestKey ?? persisted.content?.manifestKey ?? `teams/${deployConfig.slug}/published/common.json`,
376
+ lastPublishedManifestRevision: persisted.content?.lastPublishedManifestRevision ?? defaults.content?.lastPublishedManifestRevision ?? null,
377
+ lastPublishedManifestSha256: persisted.content?.lastPublishedManifestSha256 ?? defaults.content?.lastPublishedManifestSha256 ?? null
378
+ },
379
+ hosting: {
380
+ ...defaults.hosting ?? {},
381
+ ...persisted.hosting ?? {},
382
+ kind: defaults.hosting?.kind ?? persisted.hosting?.kind ?? "self_hosted_project",
383
+ registration: defaults.hosting?.registration ?? persisted.hosting?.registration ?? "none",
384
+ marketBaseUrl: defaults.hosting?.marketBaseUrl ?? persisted.hosting?.marketBaseUrl ?? null,
385
+ teamId: defaults.hosting?.teamId ?? persisted.hosting?.teamId ?? deployConfig.slug,
386
+ projectId: defaults.hosting?.projectId ?? persisted.hosting?.projectId ?? deployConfig.slug
387
+ },
388
+ pages: {
389
+ ...defaults.pages ?? {},
390
+ ...persisted.pages ?? {},
391
+ projectName: defaults.pages?.projectName ?? persisted.pages?.projectName ?? null,
392
+ productionBranch: defaults.pages?.productionBranch ?? persisted.pages?.productionBranch ?? "main",
393
+ stagingBranch: defaults.pages?.stagingBranch ?? persisted.pages?.stagingBranch ?? "staging",
394
+ buildOutputDir: defaults.pages?.buildOutputDir ?? persisted.pages?.buildOutputDir ?? "dist",
395
+ url: persisted.pages?.url ?? defaults.pages?.url ?? null
396
+ },
280
397
  readiness: {
281
398
  ...defaults.readiness,
282
399
  ...persisted.readiness ?? {}
@@ -299,12 +416,17 @@ function loadDeployState(tenantRoot, deployConfig, options = {}) {
299
416
  workerName: defaultService.workerName ?? persistedService.workerName ?? null,
300
417
  rootDir: defaultService.rootDir ?? persistedService.rootDir ?? null,
301
418
  environment: defaultService.environment ?? persistedService.environment ?? null,
419
+ schedule: defaultService.schedule ?? persistedService.schedule ?? null,
302
420
  publicBaseUrl: defaultService.publicBaseUrl ?? persistedService.publicBaseUrl ?? null,
303
- lastDeployedUrl: persistedService.lastDeployedUrl ?? defaultService.publicBaseUrl ?? null
421
+ lastDeployedUrl: persistedService.lastDeployedUrl ?? defaultService.publicBaseUrl ?? null,
422
+ lastScheduleSyncAt: persistedService.lastScheduleSyncAt ?? defaultService.lastScheduleSyncAt ?? null
304
423
  }
305
424
  ];
306
425
  })
307
- )
426
+ ),
427
+ railwaySchedules: {
428
+ ...persisted.railwaySchedules ?? {}
429
+ }
308
430
  };
309
431
  if (target.kind === "branch" && !merged.lastDeployedUrl) {
310
432
  merged.lastDeployedUrl = targetWorkersDevUrl(merged.workerName);
@@ -330,6 +452,9 @@ function buildWranglerConfigContents(tenantRoot, deployConfig, state, options =
330
452
  const assetsDirectory = relativeFromGeneratedRoot(resolve(tenantRoot, "dist"), generatedRoot);
331
453
  const migrationsDir = relativeFromGeneratedRoot(resolve(tenantRoot, "migrations"), generatedRoot);
332
454
  const vars = buildPublicVars(deployConfig);
455
+ const r2Config = deployConfig.cloudflare.r2;
456
+ const r2Binding = r2Config?.binding ?? "TREESEED_CONTENT_BUCKET";
457
+ const r2BucketName = r2Config?.bucketName ?? `${deployConfig.slug}-content`;
333
458
  return [
334
459
  `name = ${renderTomlString(workerName)}`,
335
460
  `compatibility_date = ${renderTomlString(DEFAULT_COMPATIBILITY_DATE)}`,
@@ -360,7 +485,13 @@ function buildWranglerConfigContents(tenantRoot, deployConfig, state, options =
360
485
  `database_id = ${renderTomlString(state.d1Databases.SITE_DATA_DB.databaseId)}`,
361
486
  `preview_database_id = ${renderTomlString(state.d1Databases.SITE_DATA_DB.previewDatabaseId ?? state.d1Databases.SITE_DATA_DB.databaseId)}`,
362
487
  `migrations_dir = ${renderTomlString(migrationsDir)}`,
363
- ""
488
+ "",
489
+ ...r2Config ? [
490
+ "[[r2_buckets]]",
491
+ `binding = ${renderTomlString(r2Binding)}`,
492
+ `bucket_name = ${renderTomlString(r2BucketName)}`,
493
+ ""
494
+ ] : []
364
495
  ].join("\n");
365
496
  }
366
497
  function ensureGeneratedWranglerConfig(tenantRoot, options = {}) {
@@ -379,6 +510,7 @@ function ensureGeneratedWranglerConfig(tenantRoot, options = {}) {
379
510
  }
380
511
  const secretMap = buildSecretMap(deployConfig, state);
381
512
  state.generatedSecrets.TREESEED_FORM_TOKEN_SECRET = secretMap.TREESEED_FORM_TOKEN_SECRET;
513
+ state.generatedSecrets.TREESEED_EDITORIAL_PREVIEW_SECRET = secretMap.TREESEED_EDITORIAL_PREVIEW_SECRET;
382
514
  writeDeployState(tenantRoot, state, { target });
383
515
  return { wranglerPath, deployConfig, state, manifestFingerprint, target };
384
516
  }
@@ -429,6 +561,33 @@ function listD1Databases(tenantRoot, env) {
429
561
  });
430
562
  return parseWranglerJsonOutput(result, "D1 list");
431
563
  }
564
+ function listQueues(tenantRoot, env) {
565
+ const result = runWrangler(["queues", "list", "--json"], {
566
+ cwd: tenantRoot,
567
+ capture: true,
568
+ env,
569
+ allowFailure: true
570
+ });
571
+ return result.status === 0 ? parseWranglerJsonOutput(result, "Queues list") : [];
572
+ }
573
+ function listR2Buckets(tenantRoot, env) {
574
+ const result = runWrangler(["r2", "bucket", "list", "--json"], {
575
+ cwd: tenantRoot,
576
+ capture: true,
577
+ env,
578
+ allowFailure: true
579
+ });
580
+ return result.status === 0 ? parseWranglerJsonOutput(result, "R2 bucket list") : [];
581
+ }
582
+ function listPagesProjects(tenantRoot, env) {
583
+ const result = runWrangler(["pages", "project", "list", "--json"], {
584
+ cwd: tenantRoot,
585
+ capture: true,
586
+ env,
587
+ allowFailure: true
588
+ });
589
+ return result.status === 0 ? parseWranglerJsonOutput(result, "Pages project list") : [];
590
+ }
432
591
  function isPlaceholderResourceId(value) {
433
592
  if (!value || typeof value !== "string") {
434
593
  return true;
@@ -441,11 +600,25 @@ function buildProvisioningSummary(deployConfig, state, target) {
441
600
  workerName: state.workerName ?? targetWorkerName(deployConfig, target),
442
601
  siteUrl: target.kind === "branch" ? targetWorkersDevUrl(state.workerName) : deployConfig.siteUrl,
443
602
  accountId: deployConfig.cloudflare.accountId,
603
+ pages: state.pages ?? null,
444
604
  formGuardKv: state.kvNamespaces.FORM_GUARD_KV,
445
605
  sessionKv: state.kvNamespaces.SESSION,
446
- siteDataDb: state.d1Databases.SITE_DATA_DB
606
+ siteDataDb: state.d1Databases.SITE_DATA_DB,
607
+ queue: state.queues?.agentWork ?? null,
608
+ content: state.content ?? null
447
609
  };
448
610
  }
611
+ function queueName(entry) {
612
+ return entry?.queue_name ?? entry?.queueName ?? entry?.name ?? null;
613
+ }
614
+ function queueId(entry) {
615
+ return entry?.queue_id ?? entry?.queueId ?? entry?.id ?? entry?.uuid ?? null;
616
+ }
617
+ function hasProvisionedCloudflareResources(state) {
618
+ return Boolean(
619
+ state?.pages?.projectName && state?.pages?.url && state?.d1Databases?.SITE_DATA_DB?.databaseId && state?.kvNamespaces?.FORM_GUARD_KV?.id && state?.kvNamespaces?.SESSION?.id && state?.queues?.agentWork?.name && state?.content?.bucketName
620
+ );
621
+ }
449
622
  function buildDestroySummary(deployConfig, state, target) {
450
623
  return buildProvisioningSummary(deployConfig, state, target);
451
624
  }
@@ -462,6 +635,18 @@ function missingTurnstileRequirements() {
462
635
  }
463
636
  return issues;
464
637
  }
638
+ function missingContentRuntimeRequirements(deployConfig) {
639
+ const issues = [];
640
+ if (deployConfig.providers?.content?.runtime === "team_scoped_r2_overlay") {
641
+ if (!deployConfig.cloudflare.r2?.bucketName) {
642
+ issues.push("Set cloudflare.r2.bucketName before deploying team-scoped hosted content.");
643
+ }
644
+ if (!envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET")) {
645
+ issues.push("Set TREESEED_EDITORIAL_PREVIEW_SECRET before deploying team-scoped hosted content.");
646
+ }
647
+ }
648
+ return issues;
649
+ }
465
650
  function collectMissingDeployInputs(tenantRoot) {
466
651
  const deployConfig = loadTenantDeployConfig(tenantRoot);
467
652
  const missing = [];
@@ -486,6 +671,13 @@ function collectMissingDeployInputs(tenantRoot) {
486
671
  message: "Turnstile secret key is missing for deploy."
487
672
  });
488
673
  }
674
+ if (deployConfig.providers?.content?.runtime === "team_scoped_r2_overlay" && !envOrNull("TREESEED_EDITORIAL_PREVIEW_SECRET")) {
675
+ missing.push({
676
+ key: "TREESEED_EDITORIAL_PREVIEW_SECRET",
677
+ label: "Editorial preview signing secret",
678
+ message: "Editorial preview signing secret is missing for deploy."
679
+ });
680
+ }
489
681
  return missing;
490
682
  }
491
683
  async function promptForMissingDeployInputs(tenantRoot) {
@@ -525,6 +717,7 @@ function validateDeployPrerequisites(tenantRoot, { requireRemote = true } = {})
525
717
  }
526
718
  if (requireRemote) {
527
719
  issues.push(...missingTurnstileRequirements());
720
+ issues.push(...missingContentRuntimeRequirements(deployConfig));
528
721
  const result = runWrangler(["whoami"], {
529
722
  cwd: tenantRoot,
530
723
  allowFailure: true,
@@ -731,6 +924,9 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
731
924
  const dryRun = options.dryRun ?? false;
732
925
  const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
733
926
  const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
927
+ const queues = dryRun ? [] : listQueues(tenantRoot, env);
928
+ const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
929
+ const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
734
930
  const ensureKv = (binding) => {
735
931
  const current = state.kvNamespaces[binding];
736
932
  if (current?.id && !isPlaceholderResourceId(current.id)) {
@@ -786,12 +982,103 @@ function provisionCloudflareResources(tenantRoot, options = {}) {
786
982
  current.databaseId = created.uuid;
787
983
  current.previewDatabaseId = created.previewDatabaseUuid ?? created.uuid;
788
984
  };
985
+ const ensureQueue = () => {
986
+ const current = state.queues?.agentWork;
987
+ if (!current?.name) {
988
+ return;
989
+ }
990
+ const exists = queues.find((entry) => queueName(entry) === current.name);
991
+ if (exists) {
992
+ current.queueId = queueId(exists);
993
+ const currentDlq = current.dlqName ? queues.find((entry) => queueName(entry) === current.dlqName) : null;
994
+ current.dlqId = queueId(currentDlq);
995
+ return;
996
+ }
997
+ if (dryRun) {
998
+ current.queueId = `dryrun-${current.name}`;
999
+ current.dlqId = current.dlqName ? `dryrun-${current.dlqName}` : null;
1000
+ return;
1001
+ }
1002
+ runWrangler(["queues", "create", current.name], {
1003
+ cwd: tenantRoot,
1004
+ capture: true,
1005
+ env
1006
+ });
1007
+ if (current.dlqName && !queues.find((entry) => queueName(entry) === current.dlqName)) {
1008
+ runWrangler(["queues", "create", current.dlqName], {
1009
+ cwd: tenantRoot,
1010
+ capture: true,
1011
+ env
1012
+ });
1013
+ }
1014
+ const refreshed = listQueues(tenantRoot, env);
1015
+ const created = refreshed.find((entry) => queueName(entry) === current.name);
1016
+ current.queueId = queueId(created);
1017
+ const createdDlq = current.dlqName ? refreshed.find((entry) => queueName(entry) === current.dlqName) : null;
1018
+ current.dlqId = queueId(createdDlq);
1019
+ };
1020
+ const ensureR2Bucket = () => {
1021
+ const bucketName = state.content?.bucketName;
1022
+ if (!bucketName) {
1023
+ return;
1024
+ }
1025
+ const exists = buckets.find((entry) => entry?.name === bucketName);
1026
+ if (exists) {
1027
+ return;
1028
+ }
1029
+ if (dryRun) {
1030
+ return;
1031
+ }
1032
+ runWrangler(["r2", "bucket", "create", bucketName], {
1033
+ cwd: tenantRoot,
1034
+ capture: true,
1035
+ env
1036
+ });
1037
+ };
1038
+ const ensurePagesProject = () => {
1039
+ const current = state.pages;
1040
+ if (!current?.projectName) {
1041
+ return;
1042
+ }
1043
+ const exists = pagesProjects.find((entry) => entry?.name === current.projectName);
1044
+ if (exists) {
1045
+ current.url = exists.subdomain ? `https://${exists.subdomain}` : current.url ?? `https://${current.projectName}.pages.dev`;
1046
+ return;
1047
+ }
1048
+ if (dryRun) {
1049
+ current.url = `https://${current.projectName}.pages.dev`;
1050
+ return;
1051
+ }
1052
+ runWrangler([
1053
+ "pages",
1054
+ "project",
1055
+ "create",
1056
+ current.projectName,
1057
+ "--production-branch",
1058
+ target.kind === "persistent" && target.scope === "prod" ? current.productionBranch ?? "main" : current.stagingBranch ?? "staging"
1059
+ ], {
1060
+ cwd: tenantRoot,
1061
+ capture: true,
1062
+ env
1063
+ });
1064
+ current.url = `https://${current.projectName}.pages.dev`;
1065
+ };
789
1066
  ensureKv("FORM_GUARD_KV");
790
1067
  ensureKv("SESSION");
791
1068
  ensureD1();
1069
+ ensureQueue();
1070
+ ensureR2Bucket();
1071
+ ensurePagesProject();
1072
+ state.readiness.configured = true;
1073
+ state.readiness.provisioned = hasProvisionedCloudflareResources(state);
1074
+ state.readiness.deployable = state.readiness.provisioned === true;
792
1075
  state.readiness.initialized = true;
793
1076
  state.readiness.initializedAt = (/* @__PURE__ */ new Date()).toISOString();
794
1077
  state.readiness.lastValidatedAt = state.readiness.initializedAt;
1078
+ state.readiness.lastValidationSummary = {
1079
+ cloudflare: state.readiness.provisioned === true ? "ready" : "incomplete",
1080
+ railway: "configured"
1081
+ };
795
1082
  writeDeployState(tenantRoot, state, { target });
796
1083
  return buildProvisioningSummary(deployConfig, state, target);
797
1084
  }
@@ -827,11 +1114,58 @@ function syncCloudflareSecrets(tenantRoot, options = {}) {
827
1114
  }
828
1115
  state.generatedSecrets = {
829
1116
  ...state.generatedSecrets ?? {},
830
- TREESEED_FORM_TOKEN_SECRET: secrets.TREESEED_FORM_TOKEN_SECRET ?? state.generatedSecrets?.TREESEED_FORM_TOKEN_SECRET
1117
+ TREESEED_FORM_TOKEN_SECRET: secrets.TREESEED_FORM_TOKEN_SECRET ?? state.generatedSecrets?.TREESEED_FORM_TOKEN_SECRET,
1118
+ TREESEED_EDITORIAL_PREVIEW_SECRET: secrets.TREESEED_EDITORIAL_PREVIEW_SECRET ?? state.generatedSecrets?.TREESEED_EDITORIAL_PREVIEW_SECRET
831
1119
  };
832
1120
  writeDeployState(tenantRoot, state, { target });
833
1121
  return synced;
834
1122
  }
1123
+ function verifyProvisionedCloudflareResources(tenantRoot, options = {}) {
1124
+ const target = normalizeTarget(options.scope ?? options.target ?? "prod");
1125
+ const deployConfig = loadTenantDeployConfig(tenantRoot);
1126
+ const state = loadDeployState(tenantRoot, deployConfig, { target });
1127
+ const env = {
1128
+ CLOUDFLARE_ACCOUNT_ID: deployConfig.cloudflare.accountId
1129
+ };
1130
+ const dryRun = options.dryRun ?? false;
1131
+ const kvNamespaces = dryRun ? [] : listKvNamespaces(tenantRoot, env);
1132
+ const d1Databases = dryRun ? [] : listD1Databases(tenantRoot, env);
1133
+ const queues = dryRun ? [] : listQueues(tenantRoot, env);
1134
+ const buckets = dryRun ? [] : listR2Buckets(tenantRoot, env);
1135
+ const pagesProjects = dryRun ? [] : listPagesProjects(tenantRoot, env);
1136
+ const checks = {
1137
+ pages: Boolean(state.pages?.projectName && pagesProjects.find((entry) => entry?.name === state.pages.projectName)),
1138
+ formGuardKv: Boolean(state.kvNamespaces?.FORM_GUARD_KV?.name && kvNamespaces.find((entry) => entry?.title === state.kvNamespaces.FORM_GUARD_KV.name)),
1139
+ sessionKv: Boolean(state.kvNamespaces?.SESSION?.name && kvNamespaces.find((entry) => entry?.title === state.kvNamespaces.SESSION.name)),
1140
+ d1: Boolean(state.d1Databases?.SITE_DATA_DB?.databaseName && d1Databases.find((entry) => entry?.name === state.d1Databases.SITE_DATA_DB.databaseName)),
1141
+ queue: Boolean(state.queues?.agentWork?.name && queues.find((entry) => queueName(entry) === state.queues.agentWork.name)),
1142
+ dlq: !state.queues?.agentWork?.dlqName || Boolean(queues.find((entry) => queueName(entry) === state.queues.agentWork.dlqName)),
1143
+ r2: Boolean(state.content?.bucketName && buckets.find((entry) => entry?.name === state.content.bucketName))
1144
+ };
1145
+ const ok = dryRun ? true : Object.values(checks).every(Boolean);
1146
+ state.readiness.configured = true;
1147
+ state.readiness.provisioned = ok;
1148
+ state.readiness.deployable = ok;
1149
+ state.readiness.lastValidatedAt = (/* @__PURE__ */ new Date()).toISOString();
1150
+ state.readiness.lastValidationSummary = checks;
1151
+ const liveQueue = queues.find((entry) => queueName(entry) === state.queues?.agentWork?.name);
1152
+ if (state.queues?.agentWork) {
1153
+ state.queues.agentWork.queueId = queueId(liveQueue) ?? state.queues.agentWork.queueId ?? null;
1154
+ const liveDlq = queues.find((entry) => queueName(entry) === state.queues.agentWork.dlqName);
1155
+ state.queues.agentWork.dlqId = queueId(liveDlq) ?? state.queues.agentWork.dlqId ?? null;
1156
+ }
1157
+ const livePages = pagesProjects.find((entry) => entry?.name === state.pages?.projectName);
1158
+ if (state.pages && livePages?.subdomain) {
1159
+ state.pages.url = `https://${livePages.subdomain}`;
1160
+ }
1161
+ writeDeployState(tenantRoot, state, { target });
1162
+ return {
1163
+ ok,
1164
+ target: deployTargetLabel(target),
1165
+ checks,
1166
+ state
1167
+ };
1168
+ }
835
1169
  function runRemoteD1Migrations(tenantRoot, options = {}) {
836
1170
  const target = normalizeTarget(options.scope ?? options.target ?? "prod");
837
1171
  const { wranglerPath, deployConfig, state } = ensureGeneratedWranglerConfig(tenantRoot, { target });
@@ -853,6 +1187,9 @@ function markDeploymentInitialized(tenantRoot, options = {}) {
853
1187
  const state = loadDeployState(tenantRoot, deployConfig, { target });
854
1188
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
855
1189
  state.readiness.initialized = true;
1190
+ state.readiness.configured = true;
1191
+ state.readiness.provisioned = hasProvisionedCloudflareResources(state);
1192
+ state.readiness.deployable = state.readiness.provisioned === true;
856
1193
  state.readiness.initializedAt = state.readiness.initializedAt ?? timestamp;
857
1194
  state.readiness.lastValidatedAt = timestamp;
858
1195
  state.readiness.lastConfigFingerprint = state.lastManifestFingerprint ?? state.readiness.lastConfigFingerprint;
@@ -912,6 +1249,9 @@ function finalizeDeploymentState(tenantRoot, options = {}) {
912
1249
  const history = Array.isArray(state.deploymentHistory) ? state.deploymentHistory : [];
913
1250
  state.deploymentHistory = [...history, nextHistoryEntry].slice(-20);
914
1251
  state.readiness.initialized = true;
1252
+ state.readiness.configured = true;
1253
+ state.readiness.provisioned = hasProvisionedCloudflareResources(state);
1254
+ state.readiness.deployable = state.readiness.provisioned === true;
915
1255
  state.readiness.lastValidatedAt = state.lastDeploymentTimestamp;
916
1256
  for (const result of options.serviceResults ?? []) {
917
1257
  if (!result?.service || !state.services?.[result.service]) {
@@ -977,5 +1317,6 @@ export {
977
1317
  syncCloudflareSecrets,
978
1318
  validateDeployPrerequisites,
979
1319
  validateDestroyPrerequisites,
1320
+ verifyProvisionedCloudflareResources,
980
1321
  writeDeployState
981
1322
  };
@@ -14,17 +14,42 @@ export declare function requiredGitHubSecrets(tenantRoot: any): string[];
14
14
  export declare function renderDeployWorkflow({ workingDirectory }: {
15
15
  workingDirectory: any;
16
16
  }): string;
17
+ export declare function renderHostedProjectWorkflow({ workingDirectory }: {
18
+ workingDirectory: any;
19
+ }): string;
17
20
  export declare function ensureDeployWorkflow(tenantRoot: any): {
18
21
  workflowPath: string;
19
22
  changed: boolean;
20
23
  workingDirectory: string;
21
24
  mode: string;
22
25
  } | {
26
+ workingDirectory: string;
27
+ workflowPath: string;
28
+ changed: boolean;
29
+ mode?: undefined;
30
+ };
31
+ export declare function ensureHostedProjectWorkflow(tenantRoot: any): {
23
32
  workflowPath: string;
24
33
  changed: boolean;
25
34
  workingDirectory: string;
35
+ mode: string;
36
+ } | {
37
+ workingDirectory: string;
38
+ workflowPath: string;
39
+ changed: boolean;
26
40
  mode?: undefined;
27
41
  };
42
+ export declare function ensureStandardizedGitHubWorkflows(tenantRoot: any): ({
43
+ workflowPath: string;
44
+ changed: boolean;
45
+ workingDirectory: string;
46
+ mode: string;
47
+ } | {
48
+ workingDirectory: string;
49
+ workflowPath: string;
50
+ changed: boolean;
51
+ mode?: undefined;
52
+ })[];
28
53
  export declare function listGitHubSecretNames(repository: any, tenantRoot: any): Set<unknown>;
29
54
  export declare function listGitHubVariableNames(repository: any, tenantRoot: any): Set<unknown>;
30
55
  export declare function formatMissingSecretsReport(repository: any, missingSecrets: any, reason?: string): string;
@@ -91,11 +116,22 @@ export declare function ensureGitHubDeployAutomation(tenantRoot: any, { dryRun }
91
116
  workingDirectory: string;
92
117
  mode: string;
93
118
  } | {
119
+ workingDirectory: string;
94
120
  workflowPath: string;
95
121
  changed: boolean;
96
- workingDirectory: string;
97
122
  mode?: undefined;
98
123
  };
124
+ workflows: ({
125
+ workflowPath: string;
126
+ changed: boolean;
127
+ workingDirectory: string;
128
+ mode: string;
129
+ } | {
130
+ workingDirectory: string;
131
+ workflowPath: string;
132
+ changed: boolean;
133
+ mode?: undefined;
134
+ })[];
99
135
  secrets: {
100
136
  existing: never[];
101
137
  created: never[];