@treeseed/sdk 0.10.24 → 0.10.26

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 (39) hide show
  1. package/dist/index.d.ts +12 -2
  2. package/dist/index.js +42 -1
  3. package/dist/market-client.d.ts +23 -0
  4. package/dist/market-client.js +30 -0
  5. package/dist/operations/providers/default.js +103 -10
  6. package/dist/operations/repository-operations.d.ts +6 -1
  7. package/dist/operations/repository-operations.js +44 -0
  8. package/dist/operations/services/config-runtime.d.ts +24 -9
  9. package/dist/operations/services/config-runtime.js +60 -12
  10. package/dist/operations/services/deploy.js +6 -1
  11. package/dist/operations/services/hub-launch.js +1 -0
  12. package/dist/operations/services/hub-provider-launch.d.ts +11 -1
  13. package/dist/operations/services/hub-provider-launch.js +81 -8
  14. package/dist/operations/services/project-host-operations.d.ts +153 -0
  15. package/dist/operations/services/project-host-operations.js +365 -0
  16. package/dist/operations/services/project-platform.d.ts +198 -193
  17. package/dist/operations/services/project-platform.js +29 -14
  18. package/dist/operations/services/railway-deploy.d.ts +3 -0
  19. package/dist/operations/services/railway-deploy.js +74 -35
  20. package/dist/operations/services/release-candidate.js +8 -2
  21. package/dist/operations/services/template-host-bindings.d.ts +68 -0
  22. package/dist/operations/services/template-host-bindings.js +400 -0
  23. package/dist/operations/services/template-registry.d.ts +22 -2
  24. package/dist/operations/services/template-registry.js +60 -3
  25. package/dist/operations/services/template-secret-sync.d.ts +97 -0
  26. package/dist/operations/services/template-secret-sync.js +292 -0
  27. package/dist/platform/environment.d.ts +3 -0
  28. package/dist/project-workflow.d.ts +7 -1
  29. package/dist/scripts/scaffold-site.js +3 -2
  30. package/dist/scripts/test-scaffold.js +2 -1
  31. package/dist/sdk-types.d.ts +87 -0
  32. package/dist/sdk-types.js +29 -0
  33. package/dist/template-catalog.js +3 -1
  34. package/dist/template-launch-requirements.d.ts +118 -0
  35. package/dist/template-launch-requirements.js +759 -0
  36. package/dist/template-launch-ui.d.ts +85 -0
  37. package/dist/template-launch-ui.js +189 -0
  38. package/dist/treeseed/template-catalog/catalog.fixture.json +330 -3
  39. package/package.json +13 -1
@@ -2123,6 +2123,7 @@ async function syncTreeseedGitHubEnvironment({
2123
2123
  dryRun = false,
2124
2124
  repository: repositoryInput,
2125
2125
  valuesOverlay = {},
2126
+ entryIds,
2126
2127
  managedHostMode = "auto",
2127
2128
  execution = "parallel",
2128
2129
  concurrency = 4,
@@ -2145,8 +2146,10 @@ async function syncTreeseedGitHubEnvironment({
2145
2146
  }) : null;
2146
2147
  const allowedSecrets = allowed ? new Set(allowed.secrets) : null;
2147
2148
  const allowedVariables = allowed ? new Set(allowed.variables) : null;
2149
+ const entryFilter = Array.isArray(entryIds) && entryIds.length > 0 ? new Set(entryIds) : null;
2148
2150
  const relevant = registry.entries.filter((entry) => {
2149
2151
  if (!entry.scopes.includes(scope)) return false;
2152
+ if (entryFilter && !entryFilter.has(entry.id)) return false;
2150
2153
  if (!managedBoundary) return true;
2151
2154
  if (entry.sensitivity === "secret") {
2152
2155
  return Boolean(entry.targets.includes("github-secret") && allowedSecrets?.has(entry.id));
@@ -2218,25 +2221,48 @@ async function syncTreeseedGitHubEnvironment({
2218
2221
  repository,
2219
2222
  scope,
2220
2223
  environment,
2224
+ entryIds: entryFilter ? [...entryFilter] : void 0,
2221
2225
  ...synced
2222
2226
  };
2223
2227
  }
2224
- function syncTreeseedCloudflareEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
2225
- const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
2228
+ function syncTreeseedCloudflareEnvironment({
2229
+ tenantRoot,
2230
+ scope = "prod",
2231
+ dryRun = false,
2232
+ valuesOverlay = {},
2233
+ entryIds,
2234
+ onProgress
2235
+ }) {
2236
+ const values = {
2237
+ ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
2238
+ ...nonEmptyEnvironmentValues(valuesOverlay)
2239
+ };
2226
2240
  const target = createPersistentDeployTarget(scope);
2241
+ const progress = (message, stream = "stdout") => onProgress?.(message, stream);
2227
2242
  for (const [key, value] of Object.entries(values)) {
2228
2243
  if (typeof value === "string" && value.length > 0) {
2229
2244
  process.env[key] = value;
2230
2245
  }
2231
2246
  }
2247
+ progress(`[${scope}][cloudflare][config] Generating Wrangler config...`);
2232
2248
  const { wranglerPath } = ensureGeneratedWranglerConfig(tenantRoot, { target });
2233
- const syncedSecrets = syncCloudflareSecrets(tenantRoot, { dryRun, target });
2234
2249
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
2235
- const cloudflareVars = registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("cloudflare-var")).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
2250
+ const entryFilter = Array.isArray(entryIds) && entryIds.length > 0 ? new Set(entryIds) : null;
2251
+ const cloudflareSecrets = Object.fromEntries(registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("cloudflare-secret") && (!entryFilter || entryFilter.has(entry.id))).map((entry) => [entry.id, values[entry.id]]).filter(([, value]) => typeof value === "string" && value.length > 0));
2252
+ progress(`[${scope}][cloudflare][sync] Syncing Cloudflare secrets...`);
2253
+ const syncedSecrets = syncCloudflareSecrets(tenantRoot, {
2254
+ dryRun,
2255
+ target,
2256
+ extraSecrets: cloudflareSecrets,
2257
+ entryIds
2258
+ });
2259
+ const cloudflareVars = registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes("cloudflare-var") && (!entryFilter || entryFilter.has(entry.id))).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
2260
+ progress(`[${scope}][cloudflare][sync] Complete: ${syncedSecrets.length} secrets, ${cloudflareVars.length} vars.`);
2236
2261
  return {
2237
2262
  scope,
2238
2263
  target,
2239
2264
  wranglerPath,
2265
+ entryIds: entryFilter ? [...entryFilter] : void 0,
2240
2266
  secrets: syncedSecrets,
2241
2267
  varsManagedByWranglerConfig: cloudflareVars
2242
2268
  };
@@ -2245,18 +2271,30 @@ function environmentEntryTargetsService(entry, serviceKey) {
2245
2271
  const targets = Array.isArray(entry.serviceTargets) ? entry.serviceTargets.map((value) => String(value).trim()).filter(Boolean) : [];
2246
2272
  return targets.length === 0 || targets.includes(serviceKey);
2247
2273
  }
2248
- function railwayEnvironmentEntryIdsForService(registry, values, scope, target, serviceKey) {
2249
- return registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes(target) && environmentEntryTargetsService(entry, serviceKey)).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
2274
+ function railwayEnvironmentEntryIdsForService(registry, values, scope, target, serviceKey, entryFilter = null) {
2275
+ return registry.entries.filter((entry) => entry.scopes.includes(scope) && entry.targets.includes(target) && (!entryFilter || entryFilter.has(entry.id)) && environmentEntryTargetsService(entry, serviceKey)).map((entry) => entry.id).filter((key) => typeof values[key] === "string" && values[key].length > 0);
2250
2276
  }
2251
- function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = false } = {}) {
2277
+ function syncTreeseedRailwayEnvironment({
2278
+ tenantRoot,
2279
+ scope = "prod",
2280
+ dryRun = false,
2281
+ valuesOverlay = {},
2282
+ entryIds,
2283
+ onProgress
2284
+ }) {
2252
2285
  const config = syncManagedServiceSettingsFromDeployConfig(tenantRoot);
2253
- const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
2286
+ const values = {
2287
+ ...resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
2288
+ ...nonEmptyEnvironmentValues(valuesOverlay)
2289
+ };
2254
2290
  const deployConfig = loadCliDeployConfig(tenantRoot);
2255
2291
  const marketDatabaseService = deployConfig.services?.marketDatabase;
2256
2292
  const marketDatabaseBaseName = typeof marketDatabaseService?.railway?.serviceName === "string" && marketDatabaseService.railway.serviceName.trim() ? marketDatabaseService.railway.serviceName.trim() : `${deployConfig.slug ?? "treeseed-market"}-postgres`;
2257
2293
  const marketDatabaseServiceName = `${marketDatabaseBaseName.replace(/-(staging|prod|production)$/u, "")}-${scope === "prod" ? "prod" : scope}`;
2258
2294
  const marketDatabaseUrl = typeof values.TREESEED_MARKET_DATABASE_URL === "string" && values.TREESEED_MARKET_DATABASE_URL.length > 0 ? values.TREESEED_MARKET_DATABASE_URL : marketDatabaseService?.enabled !== false && marketDatabaseService?.provider === "railway" ? `\${{${marketDatabaseServiceName}.DATABASE_URL}}` : "";
2259
2295
  const registry = collectTreeseedEnvironmentContext(tenantRoot);
2296
+ const entryFilter = Array.isArray(entryIds) && entryIds.length > 0 ? new Set(entryIds) : null;
2297
+ const progress = (message, stream = "stdout") => onProgress?.(message, stream);
2260
2298
  const serviceValuesByName = /* @__PURE__ */ new Map();
2261
2299
  const services = configuredRailwayServices(tenantRoot, scope).map((service) => {
2262
2300
  const fallbackServiceName = service.key === "api" ? config.settings.services.railway.apiServiceName : service.serviceName;
@@ -2281,13 +2319,14 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
2281
2319
  rootDir: service.rootDir,
2282
2320
  baseUrl: service.publicBaseUrl ?? "(unset)",
2283
2321
  environmentName,
2284
- secrets: railwayEnvironmentEntryIdsForService(registry, serviceValues, scope, "railway-secret", service.key),
2285
- variables: railwayEnvironmentEntryIdsForService(registry, serviceValues, scope, "railway-var", service.key),
2322
+ secrets: railwayEnvironmentEntryIdsForService(registry, serviceValues, scope, "railway-secret", service.key, entryFilter),
2323
+ variables: railwayEnvironmentEntryIdsForService(registry, serviceValues, scope, "railway-var", service.key, entryFilter),
2286
2324
  dryRun
2287
2325
  };
2288
2326
  }).filter(Boolean);
2289
2327
  for (const service of services) {
2290
2328
  const serviceValues = serviceValuesByName.get(service.serviceName || service.serviceId || service.instanceKey || service.service) ?? values;
2329
+ progress(`[${scope}][railway][${service.service}] Syncing ${service.secrets.length} secrets and ${service.variables.length} variables...`);
2291
2330
  for (const key of service.secrets) {
2292
2331
  runRailway(
2293
2332
  ["variable", "set", "--service", service.serviceName || service.serviceId, "--environment", service.environmentName, "--stdin", "--skip-deploys", key],
@@ -2300,9 +2339,11 @@ function syncTreeseedRailwayEnvironment({ tenantRoot, scope = "prod", dryRun = f
2300
2339
  { cwd: service.rootDir, dryRun, input: serviceValues[key] }
2301
2340
  );
2302
2341
  }
2342
+ progress(`[${scope}][railway][${service.service}] Complete.`);
2303
2343
  }
2304
2344
  return {
2305
2345
  scope,
2346
+ entryIds: entryFilter ? [...entryFilter] : void 0,
2306
2347
  services
2307
2348
  };
2308
2349
  }
@@ -2477,7 +2518,8 @@ function formatTreeseedConfigValidationFailure(validations, scopes) {
2477
2518
  lines.push(`${scope}:`);
2478
2519
  for (const problem of [...validation.missing, ...validation.invalid]) {
2479
2520
  const targets = problem.entry.targets.length > 0 ? ` Targets: ${problem.entry.targets.join(", ")}.` : "";
2480
- lines.push(`- ${problem.id}: ${problem.message}${targets}`);
2521
+ const source = problem.entry.sourceRequirement ? ` Source: ${problem.entry.sourceRequirement}${problem.entry.sourceProvider ? ` (${problem.entry.sourceProvider})` : ""}.` : "";
2522
+ lines.push(`- ${problem.id}: ${problem.message}${targets}${source}`);
2481
2523
  }
2482
2524
  }
2483
2525
  return lines.join("\n");
@@ -2596,6 +2638,9 @@ function buildConfigEntrySnapshot(scope, entry, currentValue, suggestedValue) {
2596
2638
  purposes: [...entry.purposes],
2597
2639
  storage: entry.storage ?? "scoped",
2598
2640
  validation: entry.validation,
2641
+ sourceRequirement: entry.sourceRequirement,
2642
+ sourceHostType: entry.sourceHostType ?? null,
2643
+ sourceProvider: entry.sourceProvider ?? null,
2599
2644
  scope,
2600
2645
  sharedScopes: entry.storage === "shared" ? [...entry.scopes] : [scope],
2601
2646
  required: false,
@@ -3046,7 +3091,10 @@ function collectTreeseedPrintEnvReport({
3046
3091
  sensitivity: entry.sensitivity,
3047
3092
  value: rawValue,
3048
3093
  displayValue: rawValue ? entry.sensitivity === "secret" && !revealSecrets ? maskValue(rawValue) : rawValue : "(unset)",
3049
- source: sources[entry.id] ?? "unset"
3094
+ source: sources[entry.id] ?? "unset",
3095
+ sourceRequirement: entry.sourceRequirement,
3096
+ sourceHostType: entry.sourceHostType ?? null,
3097
+ sourceProvider: entry.sourceProvider ?? null
3050
3098
  };
3051
3099
  })
3052
3100
  };
@@ -2916,7 +2916,12 @@ function syncCloudflareSecrets(tenantRoot, options = {}) {
2916
2916
  const env = {
2917
2917
  CLOUDFLARE_ACCOUNT_ID: resolveConfiguredCloudflareAccountId(deployConfig)
2918
2918
  };
2919
- const secrets = buildSecretMap(deployConfig, state);
2919
+ const entryFilter = Array.isArray(options.entryIds) && options.entryIds.length > 0 ? new Set(options.entryIds) : null;
2920
+ const extraSecrets = options.extraSecrets && typeof options.extraSecrets === "object" ? Object.fromEntries(Object.entries(options.extraSecrets).filter(([key, value]) => (!entryFilter || entryFilter.has(key)) && typeof value === "string" && value.length > 0)) : {};
2921
+ const secrets = {
2922
+ ...buildSecretMap(deployConfig, state),
2923
+ ...extraSecrets
2924
+ };
2920
2925
  const synced = [];
2921
2926
  const dryRun = options.dryRun ?? false;
2922
2927
  for (const [key, value] of Object.entries(secrets)) {
@@ -223,6 +223,7 @@ function phaseFromProviderLaunch(entry) {
223
223
  repo_provision: "repository_create",
224
224
  content_repository: "content_repository_create",
225
225
  content_bootstrap: "starting_shape_apply",
226
+ host_binding_config: "host_binding_config",
226
227
  workflow_bootstrap: "config_sync",
227
228
  hosting_registration: "cloudflare_reconcile",
228
229
  runtime_connection: "backend_processing_connect"
@@ -1,7 +1,9 @@
1
1
  import { checkTreeseedProviderConnections, syncTreeseedGitHubEnvironment } from './config-runtime.ts';
2
2
  import { configuredRailwayServices, deployRailwayService, ensureRailwayScheduledJobs, verifyRailwayScheduledJobs } from './railway-deploy.ts';
3
+ import { type ProjectLaunchSecretSyncResult } from './template-secret-sync.ts';
3
4
  import { buildKnowledgePackMarketPackage, buildTemplateMarketPackage } from './market-packaging.ts';
4
- export type KnowledgeHubProviderLaunchFailurePhase = 'repo_provision_failed' | 'content_bootstrap_failed' | 'workflow_bootstrap_failed' | 'hosting_registration_failed' | 'runtime_connection_failed';
5
+ import type { ProjectLaunchConfigWritePlanItem, ProjectLaunchResolvedHostBinding, ProjectLaunchSecretDeploymentPlanItem } from '../../template-launch-requirements.ts';
6
+ export type KnowledgeHubProviderLaunchFailurePhase = 'repo_provision_failed' | 'content_bootstrap_failed' | 'workflow_bootstrap_failed' | 'hosting_registration_failed' | 'host_binding_secret_sync_failed' | 'runtime_connection_failed';
5
7
  export interface KnowledgeHubProviderLaunchInput {
6
8
  projectId: string;
7
9
  teamId: string;
@@ -47,6 +49,13 @@ export interface KnowledgeHubProviderLaunchInput {
47
49
  manageDns?: boolean;
48
50
  provider?: string | null;
49
51
  } | null;
52
+ hostBindings?: Record<string, ProjectLaunchResolvedHostBinding>;
53
+ hostBindingPlans?: {
54
+ configWrites?: ProjectLaunchConfigWritePlanItem[];
55
+ secretDeployment?: {
56
+ items?: ProjectLaunchSecretDeploymentPlanItem[];
57
+ };
58
+ };
50
59
  }
51
60
  export interface KnowledgeHubCloudflareHostConfig {
52
61
  CLOUDFLARE_API_TOKEN?: string;
@@ -110,6 +119,7 @@ export interface KnowledgeHubProviderLaunchResult {
110
119
  created: string[];
111
120
  };
112
121
  environmentSync?: Array<Awaited<ReturnType<typeof syncTreeseedGitHubEnvironment>>>;
122
+ hostBindingSecretSync?: ProjectLaunchSecretSyncResult | null;
113
123
  };
114
124
  cloudflare: {
115
125
  staging: ReturnType<typeof provisionCloudflareResources>;
@@ -17,8 +17,14 @@ import { configuredRailwayServices, deployRailwayService, ensureRailwayScheduled
17
17
  import { loadCliDeployConfig } from "./runtime-tools.js";
18
18
  import { templateCatalogRoot } from "./runtime-paths.js";
19
19
  import { scaffoldTemplateProject } from "./template-registry.js";
20
+ import { applyProjectLaunchHostBindingConfig } from "./template-host-bindings.js";
21
+ import {
22
+ ProjectLaunchSecretSyncError,
23
+ syncProjectLaunchHostBindingSecrets
24
+ } from "./template-secret-sync.js";
20
25
  import { buildKnowledgePackMarketPackage, buildTemplateMarketPackage, importKnowledgePack } from "./market-packaging.js";
21
26
  import { resolveTreeseedToolBinary } from "../../managed-dependencies.js";
27
+ import { TREESEED_DEFAULT_STARTER_TEMPLATE_ID } from "../../sdk-types.js";
22
28
  class KnowledgeHubProviderLaunchError extends Error {
23
29
  phase;
24
30
  phases;
@@ -609,10 +615,10 @@ function buildCloudflareHostEnvironmentOverlay(input, scope) {
609
615
  }
610
616
  function scaffoldLaunchSource(projectRoot, input) {
611
617
  const repositoryName = slugify(input.repoName ?? input.projectSlug, "project");
612
- const templateId = input.sourceKind === "template" ? slugify(input.sourceRef ?? "starter-basic", "starter-basic") : "starter-basic";
618
+ const templateId = input.sourceKind === "template" ? slugify(input.sourceRef ?? TREESEED_DEFAULT_STARTER_TEMPLATE_ID, TREESEED_DEFAULT_STARTER_TEMPLATE_ID) : TREESEED_DEFAULT_STARTER_TEMPLATE_ID;
613
619
  const templateCatalogEnv = { TREESEED_TEMPLATE_CATALOG_URL: currentTemplateCatalogUrl() };
614
620
  if (input.sourceKind === "knowledge_pack") {
615
- return scaffoldTemplateProject("starter-basic", projectRoot, {
621
+ return scaffoldTemplateProject(TREESEED_DEFAULT_STARTER_TEMPLATE_ID, projectRoot, {
616
622
  target: input.projectSlug,
617
623
  name: input.projectName,
618
624
  slug: input.projectSlug,
@@ -769,6 +775,26 @@ async function executeKnowledgeHubProviderLaunch(input, options = {}) {
769
775
  await scaffoldLaunchSource(workingRoot, input);
770
776
  ensureHostedProjectFiles(workingRoot);
771
777
  const managedDefaults = applyManagedProjectDefaults(workingRoot, input);
778
+ const hostBindingConfig = applyProjectLaunchHostBindingConfig({
779
+ projectRoot: workingRoot,
780
+ hostBindings: input.hostBindings,
781
+ hostBindingPlans: input.hostBindingPlans,
782
+ launchInput: input,
783
+ derived: {
784
+ projectSlug: slugify(input.projectSlug, "project"),
785
+ projectName: input.projectName,
786
+ repositoryName: repoName
787
+ }
788
+ });
789
+ if (hostBindingConfig.configWrites.length > 0 || hostBindingConfig.environmentWrites.length > 0) {
790
+ await appendPhase(
791
+ phases,
792
+ "host_binding_config",
793
+ "completed",
794
+ `Applied ${hostBindingConfig.configWrites.length} host config write${hostBindingConfig.configWrites.length === 1 ? "" : "s"} and ${hostBindingConfig.environmentWrites.length} environment overlay entr${hostBindingConfig.environmentWrites.length === 1 ? "y" : "ies"}.`,
795
+ reportPhase
796
+ );
797
+ }
772
798
  const seed = seedLaunchContent(workingRoot, input);
773
799
  packageSourceRoot = mkdtempSync(join(tmpdir(), `market-package-${slugify(input.projectSlug, "project")}-`));
774
800
  cpSync(workingRoot, packageSourceRoot, { recursive: true });
@@ -823,7 +849,11 @@ async function executeKnowledgeHubProviderLaunch(input, options = {}) {
823
849
  const workflows = await ensureGitHubDeployAutomation(workingRoot, { valuesOverlay: prodEnvOverlay });
824
850
  commitAndPushLaunchRepository(workingRoot, `Configure ${input.projectName} deployment`, { forcePush: !input.existingRepository?.url });
825
851
  pushDefaultWorkstreamBranch(workingRoot);
826
- let workflowSummary = { ...workflows, environmentSync: [] };
852
+ let workflowSummary = {
853
+ ...workflows,
854
+ environmentSync: [],
855
+ hostBindingSecretSync: null
856
+ };
827
857
  await appendPhase(phases, "workflow_bootstrap", "completed", "Configured GitHub workflows.", reportPhase);
828
858
  await appendPhase(phases, "hosting_registration", "running", "Provisioning Cloudflare resources and deploy state.", reportPhase);
829
859
  const staging = await reconcileTreeseedTarget({
@@ -844,11 +874,51 @@ async function executeKnowledgeHubProviderLaunch(input, options = {}) {
844
874
  ...typeof widget.secret === "string" && widget.secret.length > 0 ? { TREESEED_TURNSTILE_SECRET_KEY: widget.secret } : {}
845
875
  };
846
876
  };
847
- const githubEnvironmentSync = [];
848
- for (const [scope, valuesOverlay] of [
877
+ const scopedEnvironmentOverlays = [
849
878
  ["staging", turnstileOverlay(staging.state, stagingEnvOverlay)],
850
879
  ["prod", turnstileOverlay(prod.state, prodEnvOverlay)]
851
- ]) {
880
+ ];
881
+ const hostBindingSecretPlanItems = input.hostBindingPlans?.secretDeployment?.items ?? [];
882
+ if (hostBindingSecretPlanItems.length > 0) {
883
+ await appendPhase(
884
+ phases,
885
+ "host_binding_secret_sync",
886
+ "running",
887
+ `Syncing ${hostBindingSecretPlanItems.length} host-bound environment entr${hostBindingSecretPlanItems.length === 1 ? "y" : "ies"}.`,
888
+ reportPhase
889
+ );
890
+ try {
891
+ const hostBindingSecretSync = await syncProjectLaunchHostBindingSecrets({
892
+ projectRoot: workingRoot,
893
+ repository: repository.slug,
894
+ hostBindings: input.hostBindings,
895
+ secretDeploymentPlan: input.hostBindingPlans?.secretDeployment,
896
+ valuesByScope: Object.fromEntries(scopedEnvironmentOverlays),
897
+ onProgress: async (event) => {
898
+ await appendPhase(
899
+ phases,
900
+ `host_binding_secret_sync_${event.provider}_${event.scope}`,
901
+ event.status === "running" ? "running" : event.status,
902
+ event.message,
903
+ reportPhase
904
+ );
905
+ }
906
+ });
907
+ workflowSummary = { ...workflowSummary, hostBindingSecretSync };
908
+ await appendPhase(phases, "host_binding_secret_sync", "completed", "Synced host-bound environment entries.", reportPhase);
909
+ } catch (error) {
910
+ const result = error instanceof ProjectLaunchSecretSyncError ? error.result : null;
911
+ workflowSummary = { ...workflowSummary, hostBindingSecretSync: result };
912
+ await appendPhase(phases, "host_binding_secret_sync", "failed", error instanceof Error ? error.message : String(error), reportPhase);
913
+ throw new KnowledgeHubProviderLaunchError(
914
+ "host_binding_secret_sync_failed",
915
+ error instanceof Error ? error.message : String(error),
916
+ phases
917
+ );
918
+ }
919
+ }
920
+ const githubEnvironmentSync = [];
921
+ for (const [scope, valuesOverlay] of scopedEnvironmentOverlays) {
852
922
  githubEnvironmentSync.push(await syncTreeseedGitHubEnvironment({
853
923
  tenantRoot: workingRoot,
854
924
  scope,
@@ -960,8 +1030,11 @@ async function executeKnowledgeHubProviderLaunch(input, options = {}) {
960
1030
  };
961
1031
  } catch (error) {
962
1032
  const message = error instanceof Error ? error.message : String(error);
963
- const phase = error instanceof KnowledgeHubProviderLaunchError ? error.phase : phases.some((entry) => entry.phase === "runtime_connection" && entry.status === "running") ? "runtime_connection_failed" : phases.some((entry) => entry.phase === "hosting_registration" && entry.status === "running") ? "hosting_registration_failed" : phases.some((entry) => entry.phase === "workflow_bootstrap" && entry.status === "running") ? "workflow_bootstrap_failed" : phases.some((entry) => entry.phase === "content_bootstrap" && entry.status === "running") ? "content_bootstrap_failed" : "repo_provision_failed";
964
- await appendPhase(phases, phase.replace(/_failed$/u, ""), "failed", message, reportPhase);
1033
+ const phase = error instanceof KnowledgeHubProviderLaunchError ? error.phase : phases.some((entry) => entry.phase === "runtime_connection" && entry.status === "running") ? "runtime_connection_failed" : phases.some((entry) => entry.phase === "host_binding_secret_sync" && entry.status === "running") ? "host_binding_secret_sync_failed" : phases.some((entry) => entry.phase === "hosting_registration" && entry.status === "running") ? "hosting_registration_failed" : phases.some((entry) => entry.phase === "workflow_bootstrap" && entry.status === "running") ? "workflow_bootstrap_failed" : phases.some((entry) => entry.phase === "content_bootstrap" && entry.status === "running") ? "content_bootstrap_failed" : "repo_provision_failed";
1034
+ const failedPhase = phase.replace(/_failed$/u, "");
1035
+ if (!phases.some((entry) => entry.phase === failedPhase && entry.status === "failed")) {
1036
+ await appendPhase(phases, failedPhase, "failed", message, reportPhase);
1037
+ }
965
1038
  throw new KnowledgeHubProviderLaunchError(phase, message, phases);
966
1039
  } finally {
967
1040
  if (input.preserveWorkingTree === false) {
@@ -0,0 +1,153 @@
1
+ import type { ProjectEnvironmentName, ProjectLaunchHostBindingInput, TemplateLaunchRequirements } from '../../sdk-types.ts';
2
+ import { type PlatformRepositoryDescriptor } from '../repository-operations.ts';
3
+ import { type ProjectLaunchSecretSyncProgressEvent, type ProjectLaunchSecretSyncResult } from './template-secret-sync.ts';
4
+ import { type ProjectLaunchHostInventoryRecord, type ProjectLaunchResolvedHostBinding, type ResolveProjectLaunchHostBindingsResult } from '../../template-launch-requirements.ts';
5
+ export type ProjectHostOperationKind = 'inspect' | 'audit' | 'resync' | 'replace' | 'rotate';
6
+ export type ProjectHostOperationStatus = 'ok' | 'warning' | 'blocked';
7
+ export interface ProjectHostOperationDiagnostic {
8
+ code: string;
9
+ status: ProjectHostOperationStatus;
10
+ message: string;
11
+ requirementKey?: string;
12
+ provider?: string | null;
13
+ hostId?: string | null;
14
+ path?: string | null;
15
+ }
16
+ export interface ProjectHostRequirementBindingView {
17
+ requirementKey: string;
18
+ displayName: string;
19
+ type: string;
20
+ required: boolean;
21
+ purpose: string;
22
+ compatibleProviders: string[];
23
+ binding: {
24
+ provider: string | null;
25
+ hostId: string | null;
26
+ managedHostKey: string | null;
27
+ mode: string | null;
28
+ displayName: string | null;
29
+ ownership: string | null;
30
+ status: string | null;
31
+ environmentScopes: ProjectEnvironmentName[];
32
+ selectedBy: string | null;
33
+ selectedAt: string | null;
34
+ } | null;
35
+ configWrites: Array<{
36
+ target: string;
37
+ path: string;
38
+ valueFrom: string;
39
+ provider: string | null;
40
+ }>;
41
+ secretTargets: Array<{
42
+ env: string;
43
+ targets: string[];
44
+ scopes: ProjectEnvironmentName[];
45
+ sensitivity: string;
46
+ provider: string | null;
47
+ }>;
48
+ audit: {
49
+ status: ProjectHostOperationStatus;
50
+ diagnostics: ProjectHostOperationDiagnostic[];
51
+ marketHostId: string | null;
52
+ repositoryConfig: 'planned' | 'not_declared';
53
+ };
54
+ }
55
+ export interface ProjectHostBindingsView {
56
+ requirements: ProjectHostRequirementBindingView[];
57
+ summary: {
58
+ status: ProjectHostOperationStatus;
59
+ total: number;
60
+ blocked: number;
61
+ warnings: number;
62
+ };
63
+ diagnostics: ProjectHostOperationDiagnostic[];
64
+ }
65
+ export interface PlanProjectHostBindingOperationOptions {
66
+ kind: ProjectHostOperationKind;
67
+ requirementKey?: string | null;
68
+ currentHostBindings?: Record<string, ProjectLaunchResolvedHostBinding> | null;
69
+ replacementHostBindings?: Record<string, ProjectLaunchHostBindingInput> | null;
70
+ launchRequirements?: TemplateLaunchRequirements | null;
71
+ repositoryHosts?: ProjectLaunchHostInventoryRecord[];
72
+ teamHosts?: ProjectLaunchHostInventoryRecord[];
73
+ managedHosts?: ProjectLaunchHostInventoryRecord[];
74
+ defaultHosts?: Record<string, unknown> | null;
75
+ projectSlug?: string | null;
76
+ projectName?: string | null;
77
+ selectedAt?: string;
78
+ }
79
+ export interface PlanProjectHostBindingOperationResult {
80
+ kind: ProjectHostOperationKind;
81
+ requirementKey: string | null;
82
+ previousHostBindings: Record<string, ProjectLaunchResolvedHostBinding>;
83
+ nextHostBindings: Record<string, ProjectLaunchResolvedHostBinding>;
84
+ compatibility: ResolveProjectLaunchHostBindingsResult['compatibility'];
85
+ hostBindingPlans: {
86
+ configWrites: ResolveProjectLaunchHostBindingsResult['configWritePlan'];
87
+ secretDeployment: ResolveProjectLaunchHostBindingsResult['secretDeploymentPlan'];
88
+ };
89
+ audit: ProjectHostBindingsView;
90
+ operationSummary: {
91
+ requiresRepositoryConfigWrite: boolean;
92
+ requiresSecretSync: boolean;
93
+ changedRequirementKeys: string[];
94
+ };
95
+ }
96
+ export interface ExecuteProjectHostBindingOperationInput {
97
+ projectId?: string | null;
98
+ teamId?: string | null;
99
+ kind: ProjectHostOperationKind;
100
+ requirementKey?: string | null;
101
+ repository: PlatformRepositoryDescriptor;
102
+ hostBindings: Record<string, ProjectLaunchResolvedHostBinding>;
103
+ previousHostBindings?: Record<string, ProjectLaunchResolvedHostBinding> | null;
104
+ hostBindingPlans: PlanProjectHostBindingOperationResult['hostBindingPlans'];
105
+ operationSummary?: PlanProjectHostBindingOperationResult['operationSummary'] | null;
106
+ projectSlug?: string | null;
107
+ projectName?: string | null;
108
+ repositoryName?: string | null;
109
+ commitMessage?: string | null;
110
+ approvalRequired?: boolean;
111
+ approvalId?: string | null;
112
+ dryRun?: boolean;
113
+ }
114
+ export interface ExecuteProjectHostBindingOperationContext {
115
+ workspaceRoot: string;
116
+ environment?: string;
117
+ valuesOverlay?: Record<string, string | undefined> | null;
118
+ valuesByScope?: Record<string, Record<string, string | undefined> | null> | null;
119
+ processEnv?: Record<string, string | undefined>;
120
+ onProgress?: (event: ProjectLaunchSecretSyncProgressEvent) => void | Promise<void>;
121
+ }
122
+ export interface ExecuteProjectHostBindingOperationResult {
123
+ ok: boolean;
124
+ kind: ProjectHostOperationKind;
125
+ requirementKey: string | null;
126
+ hostBindings: Record<string, ProjectLaunchResolvedHostBinding>;
127
+ previousHostBindings: Record<string, ProjectLaunchResolvedHostBinding>;
128
+ hostBindingPlans: PlanProjectHostBindingOperationResult['hostBindingPlans'];
129
+ repository: {
130
+ operation: string;
131
+ branch: string | null;
132
+ commitSha: string | null;
133
+ changedPaths: string[];
134
+ audit: unknown;
135
+ config: unknown;
136
+ };
137
+ secretSync: ProjectLaunchSecretSyncResult | null;
138
+ summary: {
139
+ requiresRepositoryConfigWrite: boolean;
140
+ requiresSecretSync: boolean;
141
+ changedRequirementKeys: string[];
142
+ };
143
+ }
144
+ export declare function deriveProjectHostBindingsView(options: {
145
+ launchRequirements?: TemplateLaunchRequirements | null;
146
+ hostBindings?: Record<string, ProjectLaunchResolvedHostBinding> | null;
147
+ hostBindingPlans?: {
148
+ configWrites?: ResolveProjectLaunchHostBindingsResult['configWritePlan'] | null;
149
+ secretDeployment?: ResolveProjectLaunchHostBindingsResult['secretDeploymentPlan'] | null;
150
+ } | null;
151
+ }): ProjectHostBindingsView;
152
+ export declare function planProjectHostBindingOperation(options: PlanProjectHostBindingOperationOptions): PlanProjectHostBindingOperationResult;
153
+ export declare function executeProjectHostBindingOperation(input: ExecuteProjectHostBindingOperationInput, context: ExecuteProjectHostBindingOperationContext): Promise<ExecuteProjectHostBindingOperationResult>;