@treeseed/sdk 0.10.24 → 0.10.25

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
@@ -0,0 +1,400 @@
1
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+ import {
6
+ TEMPLATE_CONFIG_MERGE_STRATEGIES,
7
+ TEMPLATE_CONFIG_WRITE_TARGETS
8
+ } from "../../sdk-types.js";
9
+ function ensureDir(path) {
10
+ mkdirSync(path, { recursive: true });
11
+ }
12
+ function readStructuredFile(filePath, target) {
13
+ if (!existsSync(filePath)) {
14
+ return target === "src/env.yaml" ? { entries: {} } : {};
15
+ }
16
+ const raw = readFileSync(filePath, "utf8");
17
+ if (!raw.trim()) {
18
+ return target === "src/env.yaml" ? { entries: {} } : {};
19
+ }
20
+ const parsed = target === "package.json" ? JSON.parse(raw) : parseYaml(raw);
21
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
22
+ }
23
+ function writeStructuredFile(filePath, target, value) {
24
+ ensureDir(dirname(filePath));
25
+ const body = target === "package.json" ? `${JSON.stringify(value, null, 2)}
26
+ ` : stringifyYaml(value);
27
+ writeFileSync(filePath, body, "utf8");
28
+ }
29
+ function parseStructuredContent(content, target) {
30
+ if (!content.trim()) {
31
+ return target === "src/env.yaml" ? { entries: {} } : {};
32
+ }
33
+ const parsed = target === "package.json" ? JSON.parse(content) : parseYaml(content);
34
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
35
+ }
36
+ function stringifyStructuredContent(value, target) {
37
+ return target === "package.json" ? `${JSON.stringify(value, null, 2)}
38
+ ` : stringifyYaml(value);
39
+ }
40
+ function assertTarget(target) {
41
+ if (!TEMPLATE_CONFIG_WRITE_TARGETS.includes(target)) {
42
+ throw new Error(`Unsupported host binding config write target "${target}".`);
43
+ }
44
+ }
45
+ function safePathSegments(path) {
46
+ const segments = path.split(".");
47
+ if (segments.some((segment) => !segment || segment === "..")) {
48
+ throw new Error(`Host binding config write path "${path}" must be a safe dot path.`);
49
+ }
50
+ for (const segment of segments) {
51
+ if (!/^[A-Za-z0-9_-]+$/u.test(segment)) {
52
+ throw new Error(`Host binding config write path "${path}" contains unsafe segment "${segment}".`);
53
+ }
54
+ if (segment === "__proto__" || segment === "prototype" || segment === "constructor") {
55
+ throw new Error(`Host binding config write path "${path}" contains forbidden segment "${segment}".`);
56
+ }
57
+ }
58
+ return segments;
59
+ }
60
+ function getPath(value, path) {
61
+ let current = value;
62
+ for (const segment of path.split(".")) {
63
+ if (!current || typeof current !== "object" || Array.isArray(current)) return void 0;
64
+ current = current[segment];
65
+ }
66
+ return current;
67
+ }
68
+ function hasPath(value, path) {
69
+ let current = value;
70
+ for (const segment of path.split(".")) {
71
+ if (!current || typeof current !== "object" || Array.isArray(current) || !(segment in current)) return false;
72
+ current = current[segment];
73
+ }
74
+ return true;
75
+ }
76
+ function deepMerge(left, right) {
77
+ if (left && typeof left === "object" && !Array.isArray(left) && right && typeof right === "object" && !Array.isArray(right)) {
78
+ const result = { ...left };
79
+ for (const [key, value] of Object.entries(right)) {
80
+ result[key] = key in result ? deepMerge(result[key], value) : value;
81
+ }
82
+ return result;
83
+ }
84
+ return right;
85
+ }
86
+ function uniqueArray(left, right) {
87
+ const current = Array.isArray(left) ? left : [];
88
+ const incoming = Array.isArray(right) ? right : [right];
89
+ return [.../* @__PURE__ */ new Set([...current, ...incoming])];
90
+ }
91
+ function setDotPath(target, path, value, strategy) {
92
+ const segments = safePathSegments(path);
93
+ let current = target;
94
+ for (const segment of segments.slice(0, -1)) {
95
+ if (!current[segment] || typeof current[segment] !== "object" || Array.isArray(current[segment])) {
96
+ current[segment] = {};
97
+ }
98
+ current = current[segment];
99
+ }
100
+ const leaf = segments[segments.length - 1];
101
+ if (strategy === "replace") {
102
+ current[leaf] = value;
103
+ return;
104
+ }
105
+ if (strategy === "deep-merge") {
106
+ current[leaf] = deepMerge(current[leaf], value);
107
+ return;
108
+ }
109
+ if (strategy === "append-unique") {
110
+ current[leaf] = uniqueArray(current[leaf], value);
111
+ return;
112
+ }
113
+ throw new Error(`Unsupported host binding config merge strategy "${strategy}".`);
114
+ }
115
+ function selectedHostValue(binding, selector) {
116
+ if (selector === "provider") return binding.provider;
117
+ if (selector === "type") return binding.type;
118
+ if (selector === "id") return binding.host?.id ?? binding.hostId ?? binding.managedHostKey ?? null;
119
+ if (selector === "hostId") return binding.hostId ?? binding.host?.id ?? null;
120
+ if (selector === "managedHostKey") return binding.managedHostKey ?? null;
121
+ if (selector === "name") return binding.host?.name ?? binding.displayName ?? null;
122
+ if (selector === "displayName") return binding.displayName;
123
+ if (selector === "ownership") return binding.host?.ownership ?? null;
124
+ if (selector === "status") return binding.host?.status ?? null;
125
+ if (selector === "accountLabel") return binding.host?.accountLabel ?? null;
126
+ if (selector === "organizationOrOwner") return binding.host?.organizationOrOwner ?? null;
127
+ if (selector.startsWith("github.")) {
128
+ const field = selector.slice("github.".length);
129
+ if (field === "owner") {
130
+ return binding.host?.organizationOrOwner ?? getPath(binding.configValues, "github.owner") ?? getPath(binding.configValues, "owner") ?? null;
131
+ }
132
+ return getPath(binding.configValues, `github.${field}`) ?? null;
133
+ }
134
+ if (selector.startsWith("metadata.")) return getPath(binding.host?.metadata, selector.slice("metadata.".length)) ?? null;
135
+ if (selector.startsWith("configValues.")) return getPath(binding.configValues, selector.slice("configValues.".length)) ?? null;
136
+ if (selector.startsWith("environmentValues.")) return getPath(binding.environmentValues, selector.slice("environmentValues.".length)) ?? null;
137
+ if (selector.startsWith("secretRefs.")) return getPath(binding.secretRefs, selector.slice("secretRefs.".length)) ?? null;
138
+ throw new Error(`Unsupported selectedHost value source "${selector}".`);
139
+ }
140
+ function selectedResourceValue(binding, selector) {
141
+ if (selector === "provider") return binding.provider;
142
+ if (selector === "type") return binding.type;
143
+ if (selector === "id") return binding.host?.id ?? binding.hostId ?? binding.managedHostKey ?? null;
144
+ if (selector === "resourceId") return binding.hostId ?? binding.host?.id ?? null;
145
+ if (selector === "managedResourceKey") return binding.managedHostKey ?? null;
146
+ if (selector === "name") return binding.displayName ?? binding.host?.name ?? null;
147
+ if (selector === "displayName") return binding.displayName;
148
+ if (selector.startsWith("metadata.")) return getPath(binding.host?.metadata, selector.slice("metadata.".length)) ?? null;
149
+ if (selector.startsWith("configValues.")) return getPath(binding.configValues, selector.slice("configValues.".length)) ?? null;
150
+ if (selector.startsWith("environmentValues.")) return getPath(binding.environmentValues, selector.slice("environmentValues.".length)) ?? null;
151
+ if (selector.startsWith("secretRefs.")) return getPath(binding.secretRefs, selector.slice("secretRefs.".length)) ?? null;
152
+ throw new Error(`Unsupported selectedResource value source "${selector}".`);
153
+ }
154
+ function resolveWriteValue(write, binding, options) {
155
+ const valueFrom = write.valueFrom;
156
+ if (valueFrom.startsWith("selectedHost.")) {
157
+ if (!binding) return void 0;
158
+ return selectedHostValue(binding, valueFrom.slice("selectedHost.".length));
159
+ }
160
+ if (valueFrom.startsWith("selectedResource.")) {
161
+ if (!binding) return void 0;
162
+ return selectedResourceValue(binding, valueFrom.slice("selectedResource.".length));
163
+ }
164
+ if (valueFrom.startsWith("launchInput.domains.")) {
165
+ return getPath(options.launchInput?.domains, valueFrom.slice("launchInput.domains.".length));
166
+ }
167
+ if (valueFrom === "derived.projectSlug") {
168
+ return options.derived?.projectSlug ?? options.launchInput?.projectSlug ?? null;
169
+ }
170
+ if (valueFrom === "derived.projectName") {
171
+ return options.derived?.projectName ?? options.launchInput?.projectName ?? null;
172
+ }
173
+ if (valueFrom === "derived.repositoryName") {
174
+ return options.derived?.repositoryName ?? options.launchInput?.repoName ?? options.launchInput?.projectSlug ?? null;
175
+ }
176
+ if (valueFrom.startsWith("literal.")) {
177
+ const literal = valueFrom.slice("literal.".length);
178
+ if (literal === "true") return true;
179
+ if (literal === "false") return false;
180
+ if (literal === "null") return null;
181
+ return literal;
182
+ }
183
+ throw new Error(`Unsupported host binding config value source "${valueFrom}".`);
184
+ }
185
+ function shouldWrite(write, binding, value) {
186
+ if (write.writeWhen === "host-selected") {
187
+ return Boolean(binding?.host || binding?.hostId || binding?.managedHostKey) && value !== void 0 && value !== null && value !== "";
188
+ }
189
+ if (write.writeWhen === "feature-enabled") {
190
+ return value !== void 0 && value !== null && value !== false && value !== "";
191
+ }
192
+ return value !== void 0 && value !== null;
193
+ }
194
+ function summarizeValue(value) {
195
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
196
+ if (value === null || value === void 0) return null;
197
+ if (Array.isArray(value)) return `[${value.length} items]`;
198
+ return "{...}";
199
+ }
200
+ function normalizeTargets(targets) {
201
+ return targets.filter((target) => typeof target === "string" && target.length > 0);
202
+ }
203
+ function buildEnvironmentEntry(item, binding) {
204
+ const sourceHostType = binding?.type ?? (item.requirementKind === "host" ? item.requirementKey : null);
205
+ const sourceProvider = binding?.provider ?? null;
206
+ return {
207
+ label: item.env.replace(/^TREESEED_/u, "").replace(/_/gu, " ").toLowerCase().replace(/\b\w/gu, (letter) => letter.toUpperCase()),
208
+ group: "launch-hosts",
209
+ description: `Configuration declared by the ${item.requirementKey} launch requirement.`,
210
+ howToGet: "Resolve this value from the selected launch host or configured deployment secret manager.",
211
+ sensitivity: item.sensitivity,
212
+ targets: normalizeTargets(item.targets),
213
+ scopes: item.scopes,
214
+ requirement: item.requirementKind === "secret" ? "required" : "conditional",
215
+ purposes: ["deploy", "config"],
216
+ storage: item.sensitivity === "secret" ? "scoped" : "shared",
217
+ validation: { kind: "nonempty" },
218
+ sourcePriority: ["machine-config", "process-env"],
219
+ sourceRequirement: item.requirementKey,
220
+ sourceHostType,
221
+ sourceProvider
222
+ };
223
+ }
224
+ function applyProjectLaunchHostBindingConfig(options) {
225
+ const configWrites = options.hostBindingPlans?.configWrites ?? [];
226
+ const secretItems = options.hostBindingPlans?.secretDeployment?.items ?? [];
227
+ const hostBindings = options.hostBindings ?? {};
228
+ const documents = /* @__PURE__ */ new Map();
229
+ const summaries = [];
230
+ const environmentSummaries = [];
231
+ for (const write of configWrites) {
232
+ assertTarget(write.target);
233
+ const operation = write.mergeStrategy ?? "replace";
234
+ if (!TEMPLATE_CONFIG_MERGE_STRATEGIES.includes(operation)) {
235
+ throw new Error(`Unsupported host binding config merge strategy "${operation}".`);
236
+ }
237
+ const binding = hostBindings[write.requirementKey];
238
+ const value = resolveWriteValue(write, binding, options);
239
+ if (!shouldWrite(write, binding, value)) continue;
240
+ const document = documents.get(write.target) ?? readStructuredFile(resolve(options.projectRoot, write.target), write.target);
241
+ documents.set(write.target, document);
242
+ setDotPath(document, write.path, value, operation);
243
+ summaries.push({
244
+ target: write.target,
245
+ path: write.path,
246
+ requirementKey: write.requirementKey,
247
+ requirementKind: write.requirementKind,
248
+ provider: write.provider,
249
+ operation,
250
+ valuePreview: summarizeValue(value)
251
+ });
252
+ }
253
+ if (secretItems.length > 0) {
254
+ const target = "src/env.yaml";
255
+ const document = documents.get(target) ?? readStructuredFile(resolve(options.projectRoot, target), target);
256
+ documents.set(target, document);
257
+ document.entries = document.entries && typeof document.entries === "object" && !Array.isArray(document.entries) ? document.entries : {};
258
+ for (const item of secretItems) {
259
+ const binding = hostBindings[item.requirementKey];
260
+ document.entries[item.env] = {
261
+ ...document.entries[item.env] ?? {},
262
+ ...buildEnvironmentEntry(item, binding)
263
+ };
264
+ environmentSummaries.push({
265
+ env: item.env,
266
+ requirementKey: item.requirementKey,
267
+ requirementKind: item.requirementKind,
268
+ sourceHostType: binding?.type ?? null,
269
+ sourceProvider: binding?.provider ?? null,
270
+ sensitivity: item.sensitivity,
271
+ targets: item.targets,
272
+ scopes: item.scopes
273
+ });
274
+ }
275
+ }
276
+ for (const [target, document] of documents) {
277
+ writeStructuredFile(resolve(options.projectRoot, target), target, document);
278
+ }
279
+ return {
280
+ configWrites: summaries,
281
+ environmentWrites: environmentSummaries,
282
+ targets: [...documents.keys()]
283
+ };
284
+ }
285
+ function compareStatus(diagnostics) {
286
+ if (diagnostics.some((diagnostic) => diagnostic.status === "blocked")) return "blocked";
287
+ if (diagnostics.some((diagnostic) => diagnostic.status === "warning")) return "warning";
288
+ return "ok";
289
+ }
290
+ function auditProjectLaunchHostBindingConfig(options) {
291
+ const plannedTargets = /* @__PURE__ */ new Set();
292
+ for (const write of options.hostBindingPlans?.configWrites ?? []) {
293
+ assertTarget(write.target);
294
+ plannedTargets.add(write.target);
295
+ }
296
+ if ((options.hostBindingPlans?.secretDeployment?.items ?? []).length > 0) {
297
+ plannedTargets.add("src/env.yaml");
298
+ }
299
+ const checkedTargets = [...plannedTargets];
300
+ const tempRoot = mkdtempSync(join(tmpdir(), "treeseed-host-binding-audit-"));
301
+ const before = /* @__PURE__ */ new Map();
302
+ try {
303
+ for (const target of checkedTargets) {
304
+ const sourcePath = resolve(options.projectRoot, target);
305
+ const targetPath = resolve(tempRoot, target);
306
+ if (existsSync(sourcePath)) {
307
+ ensureDir(dirname(targetPath));
308
+ cpSync(sourcePath, targetPath);
309
+ before.set(target, readFileSync(sourcePath, "utf8"));
310
+ } else {
311
+ before.set(target, null);
312
+ }
313
+ }
314
+ const expected = applyProjectLaunchHostBindingConfig({
315
+ ...options,
316
+ projectRoot: tempRoot
317
+ });
318
+ const diagnostics = [];
319
+ const changedTargets = [];
320
+ for (const target of checkedTargets) {
321
+ const sourcePath = resolve(options.projectRoot, target);
322
+ const expectedPath = resolve(tempRoot, target);
323
+ const expectedContent = existsSync(expectedPath) ? readFileSync(expectedPath, "utf8") : null;
324
+ const actualContent = existsSync(sourcePath) ? readFileSync(sourcePath, "utf8") : null;
325
+ if (before.get(target) === null && expectedContent !== null) {
326
+ changedTargets.push(target);
327
+ diagnostics.push({
328
+ code: "missing_config_target",
329
+ status: "warning",
330
+ target,
331
+ message: `${target} is missing host-bound configuration.`
332
+ });
333
+ } else if (actualContent !== expectedContent) {
334
+ changedTargets.push(target);
335
+ diagnostics.push({
336
+ code: "stale_config_target",
337
+ status: "warning",
338
+ target,
339
+ message: `${target} does not match the current host binding plan.`
340
+ });
341
+ }
342
+ }
343
+ return {
344
+ status: compareStatus(diagnostics),
345
+ checkedTargets,
346
+ changedTargets,
347
+ diagnostics,
348
+ expected
349
+ };
350
+ } catch (error) {
351
+ const target = checkedTargets[0] ?? "treeseed.site.yaml";
352
+ const diagnostics = [{
353
+ code: "invalid_config_target",
354
+ status: "blocked",
355
+ target,
356
+ message: error instanceof Error ? error.message : String(error)
357
+ }];
358
+ return {
359
+ status: "blocked",
360
+ checkedTargets,
361
+ changedTargets: [],
362
+ diagnostics,
363
+ expected: { configWrites: [], environmentWrites: [], targets: [] }
364
+ };
365
+ } finally {
366
+ rmSync(tempRoot, { recursive: true, force: true });
367
+ }
368
+ }
369
+ function preserveProjectLaunchHostBindingConfigOverlay(options) {
370
+ assertTarget(options.target);
371
+ const configWrites = options.hostBindingPlans?.configWrites ?? [];
372
+ const shouldPreserveConfigWrites = configWrites.some((write) => write.target === options.target);
373
+ const shouldPreserveEnvironmentEntries = options.target === "src/env.yaml";
374
+ if (!shouldPreserveConfigWrites && !shouldPreserveEnvironmentEntries) {
375
+ return options.nextContent;
376
+ }
377
+ const currentDocument = parseStructuredContent(options.currentContent, options.target);
378
+ const nextDocument = parseStructuredContent(options.nextContent, options.target);
379
+ for (const write of configWrites) {
380
+ if (write.target !== options.target) continue;
381
+ safePathSegments(write.path);
382
+ if (!hasPath(currentDocument, write.path)) continue;
383
+ setDotPath(nextDocument, write.path, getPath(currentDocument, write.path), "replace");
384
+ }
385
+ if (options.target === "src/env.yaml") {
386
+ const currentEntries = currentDocument.entries && typeof currentDocument.entries === "object" && !Array.isArray(currentDocument.entries) ? currentDocument.entries : {};
387
+ nextDocument.entries = nextDocument.entries && typeof nextDocument.entries === "object" && !Array.isArray(nextDocument.entries) ? nextDocument.entries : {};
388
+ for (const [entryId, entry] of Object.entries(currentEntries)) {
389
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
390
+ if (typeof entry.sourceRequirement !== "string") continue;
391
+ nextDocument.entries[entryId] = entry;
392
+ }
393
+ }
394
+ return stringifyStructuredContent(nextDocument, options.target);
395
+ }
396
+ export {
397
+ applyProjectLaunchHostBindingConfig,
398
+ auditProjectLaunchHostBindingConfig,
399
+ preserveProjectLaunchHostBindingConfigOverlay
400
+ };
@@ -1,4 +1,5 @@
1
- import { type SdkTemplateCatalogEntry } from '../../sdk-types.ts';
1
+ import { type SdkTemplateCatalogEntry, type TemplateLaunchRequirements } from '../../sdk-types.ts';
2
+ import { type ProjectLaunchConfigWritePlanItem, type ProjectLaunchLocalHostBindingSummary, type ProjectLaunchResolvedHostBinding, type ProjectLaunchSecretDeploymentPlanItem } from '../../template-launch-requirements.ts';
2
3
  export declare const TEMPLATE_CATEGORIES: readonly ["starter", "example", "fixture", "reference-app"];
3
4
  export type TemplateCategory = (typeof TEMPLATE_CATEGORIES)[number];
4
5
  export interface TemplateVariableDefinition {
@@ -27,6 +28,7 @@ export interface TemplateManifest {
27
28
  validatedOnly?: string[];
28
29
  tenantManaged?: string[];
29
30
  };
31
+ launchRequirements?: TemplateLaunchRequirements;
30
32
  testing: {
31
33
  smokeCommand?: string;
32
34
  buildCommand?: string;
@@ -53,6 +55,22 @@ export interface StarterResolutionInput {
53
55
  contactEmail?: string | null;
54
56
  repositoryUrl?: string | null;
55
57
  discordUrl?: string | null;
58
+ hostBindingState?: StarterHostBindingState | null;
59
+ }
60
+ export interface StarterHostBindingState {
61
+ hostBindings: Record<string, ProjectLaunchResolvedHostBinding>;
62
+ hostBindingPlans: {
63
+ configWrites: ProjectLaunchConfigWritePlanItem[];
64
+ secretDeployment: {
65
+ items: ProjectLaunchSecretDeploymentPlanItem[];
66
+ };
67
+ };
68
+ hostBindingSummaries?: ProjectLaunchLocalHostBindingSummary[];
69
+ hostBindingConfig?: {
70
+ configWrites?: unknown[];
71
+ environmentWrites?: unknown[];
72
+ targets?: string[];
73
+ } | null;
56
74
  }
57
75
  interface TemplateCatalogOptions {
58
76
  cwd?: string;
@@ -66,10 +84,11 @@ export declare function validateTemplateProduct(product: Pick<TemplateProductDef
66
84
  export declare function validateAllTemplateDefinitions(options?: TemplateCatalogOptions): Promise<ResolvedTemplateDefinition[]>;
67
85
  export declare function buildTemplateReplacements(manifest: TemplateManifest, input: StarterResolutionInput): Record<string, string>;
68
86
  export declare function scaffoldTemplateProject(templateId: string, targetRoot: string, input: StarterResolutionInput, options?: TemplateCatalogOptions): Promise<TemplateProductDefinition>;
87
+ export declare function recordTemplateHostBindingState(siteRoot: string, hostBindingState: StarterHostBindingState): void;
69
88
  export declare function syncTemplateProject(siteRoot: string, options?: TemplateCatalogOptions & {
70
89
  check?: boolean;
71
90
  }): Promise<string[]>;
72
- export declare function serializeTemplateRegistryEntry(product: Pick<TemplateProductDefinition, 'id' | 'displayName' | 'description' | 'summary' | 'status' | 'featured' | 'category' | 'tags' | 'publisher' | 'templateVersion' | 'templateApiVersion' | 'minCliVersion' | 'minCoreVersion' | 'fulfillment'>): {
91
+ export declare function serializeTemplateRegistryEntry(product: Pick<TemplateProductDefinition, 'id' | 'displayName' | 'description' | 'summary' | 'status' | 'featured' | 'category' | 'tags' | 'publisher' | 'templateVersion' | 'templateApiVersion' | 'minCliVersion' | 'minCoreVersion' | 'fulfillment' | 'launchRequirements'>): {
73
92
  id: string;
74
93
  displayName: string;
75
94
  description: string;
@@ -85,6 +104,7 @@ export declare function serializeTemplateRegistryEntry(product: Pick<TemplatePro
85
104
  minCoreVersion: string | undefined;
86
105
  fulfillmentMode: "r2" | "git" | "packaged";
87
106
  source: import("../../sdk-types.ts").SdkTemplateCatalogSource;
107
+ launchRequirements: TemplateLaunchRequirements | undefined;
88
108
  };
89
109
  export declare function exportTemplateCatalogYaml(options?: TemplateCatalogOptions): Promise<string>;
90
110
  export {};
@@ -2,6 +2,10 @@ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, write
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { basename, dirname, relative, resolve } from "node:path";
4
4
  import { RemoteTemplateCatalogClient } from "../../template-catalog.js";
5
+ import {
6
+ normalizeTemplateLaunchRequirements
7
+ } from "../../template-launch-requirements.js";
8
+ import { preserveProjectLaunchHostBindingConfigOverlay } from "./template-host-bindings.js";
5
9
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
6
10
  import {
7
11
  resolveTreeseedTemplateCatalogCachePath,
@@ -98,6 +102,7 @@ function validateTemplateManifest(definition) {
98
102
  if (!existsSync(templateRoot)) {
99
103
  throw new Error(`Template ${manifest.id} is missing template/ at ${templateRoot}.`);
100
104
  }
105
+ manifest.launchRequirements = normalizeTemplateLaunchRequirements(manifest.launchRequirements, `${manifestPath}: launchRequirements`);
101
106
  validateTemplatePlaceholders(definition);
102
107
  }
103
108
  function validateTemplatePlaceholders(definition) {
@@ -426,10 +431,52 @@ async function scaffoldTemplateProject(templateId, targetRoot, input, options =
426
431
  sourceRef: definition.product.fulfillment.source.ref,
427
432
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
428
433
  lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
429
- replacements
434
+ replacements,
435
+ ...input.hostBindingState ? {
436
+ hostBindings: input.hostBindingState.hostBindings,
437
+ hostBindingPlans: input.hostBindingState.hostBindingPlans,
438
+ hostBindingSummaries: input.hostBindingState.hostBindingSummaries,
439
+ hostBindingConfig: input.hostBindingState.hostBindingConfig
440
+ } : {}
430
441
  });
431
442
  return definition.product;
432
443
  }
444
+ function recordTemplateHostBindingState(siteRoot, hostBindingState) {
445
+ const state = loadTemplateState(siteRoot);
446
+ writeTemplateState(siteRoot, {
447
+ ...state,
448
+ hostBindings: hostBindingState.hostBindings,
449
+ hostBindingPlans: hostBindingState.hostBindingPlans,
450
+ hostBindingSummaries: hostBindingState.hostBindingSummaries,
451
+ hostBindingConfig: hostBindingState.hostBindingConfig
452
+ });
453
+ }
454
+ function preserveHostBindingOverlayIfNeeded(relativePath, currentContent, nextContent, state) {
455
+ if (!state.hostBindingPlans) {
456
+ return nextContent;
457
+ }
458
+ if (relativePath !== "treeseed.site.yaml" && relativePath !== "src/env.yaml" && relativePath !== "src/manifest.yaml" && relativePath !== "package.json") {
459
+ return nextContent;
460
+ }
461
+ return preserveProjectLaunchHostBindingConfigOverlay({
462
+ target: relativePath,
463
+ currentContent,
464
+ nextContent,
465
+ hostBindingPlans: state.hostBindingPlans
466
+ });
467
+ }
468
+ function structuredTemplateContentMatches(relativePath, currentContent, nextContent) {
469
+ if (relativePath !== "treeseed.site.yaml" && relativePath !== "src/env.yaml" && relativePath !== "src/manifest.yaml" && relativePath !== "package.json") {
470
+ return false;
471
+ }
472
+ try {
473
+ const current = relativePath === "package.json" ? JSON.parse(currentContent || "{}") : parseYaml(currentContent || "{}");
474
+ const next = relativePath === "package.json" ? JSON.parse(nextContent || "{}") : parseYaml(nextContent || "{}");
475
+ return JSON.stringify(current) === JSON.stringify(next);
476
+ } catch {
477
+ return false;
478
+ }
479
+ }
433
480
  async function syncTemplateProject(siteRoot, options = {}) {
434
481
  const check = options.check === true;
435
482
  const state = loadTemplateState(siteRoot);
@@ -448,11 +495,19 @@ async function syncTemplateProject(siteRoot, options = {}) {
448
495
  }
449
496
  continue;
450
497
  }
451
- const nextContent = renderTemplateFile(sourcePath, state.replacements);
452
498
  const currentContent = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
499
+ const nextContent = preserveHostBindingOverlayIfNeeded(
500
+ relativePath,
501
+ currentContent,
502
+ renderTemplateFile(sourcePath, state.replacements),
503
+ state
504
+ );
453
505
  if (currentContent === nextContent) {
454
506
  continue;
455
507
  }
508
+ if (state.hostBindingPlans && structuredTemplateContentMatches(relativePath, currentContent, nextContent)) {
509
+ continue;
510
+ }
456
511
  if (!check) {
457
512
  ensureDir(targetPath);
458
513
  writeFileSync(targetPath, nextContent, "utf8");
@@ -492,7 +547,8 @@ function serializeTemplateRegistryEntry(product) {
492
547
  minCliVersion: product.minCliVersion,
493
548
  minCoreVersion: product.minCoreVersion,
494
549
  fulfillmentMode: product.fulfillment.mode ?? "packaged",
495
- source: product.fulfillment.source
550
+ source: product.fulfillment.source,
551
+ launchRequirements: product.launchRequirements
496
552
  };
497
553
  }
498
554
  async function exportTemplateCatalogYaml(options = {}) {
@@ -503,6 +559,7 @@ export {
503
559
  buildTemplateReplacements,
504
560
  exportTemplateCatalogYaml,
505
561
  listTemplateProducts,
562
+ recordTemplateHostBindingState,
506
563
  resolveTemplateDefinition,
507
564
  resolveTemplateProduct,
508
565
  scaffoldTemplateProject,
@@ -0,0 +1,97 @@
1
+ import { syncTreeseedCloudflareEnvironment, syncTreeseedGitHubEnvironment, syncTreeseedRailwayEnvironment, type TreeseedConfigScope } from './config-runtime.ts';
2
+ import type { ProjectLaunchResolvedHostBinding, ProjectLaunchSecretDeploymentPlanItem } from '../../template-launch-requirements.ts';
3
+ import type { ProjectLaunchRequirementKind } from '../../sdk-types.ts';
4
+ export type ProjectLaunchSecretSyncProvider = 'github' | 'cloudflare' | 'railway';
5
+ export type ProjectLaunchSecretSyncStatus = 'synced' | 'skipped' | 'failed';
6
+ export type ProjectLaunchSecretSyncProviderStatus = 'running' | 'completed' | 'failed';
7
+ export type ProjectLaunchSecretSyncTargetKind = 'github-secret' | 'github-variable' | 'cloudflare-secret' | 'cloudflare-var' | 'railway-secret' | 'railway-var';
8
+ export interface ProjectLaunchSecretValueDiagnostic {
9
+ code: 'missing_value' | 'unsupported_target';
10
+ requirementKey: string;
11
+ requirementKind: ProjectLaunchRequirementKind;
12
+ env: string;
13
+ source: string;
14
+ targets: string[];
15
+ scopes: TreeseedConfigScope[];
16
+ message: string;
17
+ }
18
+ export interface ProjectLaunchResolvedSecretValueItem {
19
+ requirementKey: string;
20
+ requirementKind: ProjectLaunchRequirementKind;
21
+ env: string;
22
+ source: string;
23
+ targets: ProjectLaunchSecretSyncTargetKind[];
24
+ scopes: TreeseedConfigScope[];
25
+ sensitivity: string;
26
+ sourceHostId?: string | null;
27
+ resolved: boolean;
28
+ }
29
+ export interface ProjectLaunchSecretValueOverlayResult {
30
+ valuesOverlay: Record<string, string>;
31
+ items: ProjectLaunchResolvedSecretValueItem[];
32
+ diagnostics: ProjectLaunchSecretValueDiagnostic[];
33
+ }
34
+ export interface ProjectLaunchSecretSyncSummaryItem {
35
+ provider: ProjectLaunchSecretSyncProvider;
36
+ scope: TreeseedConfigScope;
37
+ target: ProjectLaunchSecretSyncTargetKind;
38
+ env: string;
39
+ requirementKey: string;
40
+ requirementKind: ProjectLaunchRequirementKind;
41
+ sensitivity: string;
42
+ status: ProjectLaunchSecretSyncStatus;
43
+ error?: {
44
+ message: string;
45
+ };
46
+ }
47
+ export interface ProjectLaunchSecretSyncProviderSummary {
48
+ provider: ProjectLaunchSecretSyncProvider;
49
+ scope: TreeseedConfigScope;
50
+ entryIds: string[];
51
+ status: Exclude<ProjectLaunchSecretSyncProviderStatus, 'running'>;
52
+ error?: {
53
+ message: string;
54
+ };
55
+ }
56
+ export interface ProjectLaunchSecretSyncProgressEvent {
57
+ provider: ProjectLaunchSecretSyncProvider;
58
+ scope: TreeseedConfigScope;
59
+ status: ProjectLaunchSecretSyncProviderStatus;
60
+ entryIds: string[];
61
+ message: string;
62
+ }
63
+ export interface ProjectLaunchSecretSyncResult {
64
+ ok: boolean;
65
+ items: ProjectLaunchSecretSyncSummaryItem[];
66
+ providers: ProjectLaunchSecretSyncProviderSummary[];
67
+ diagnostics: ProjectLaunchSecretValueDiagnostic[];
68
+ }
69
+ export interface ProjectLaunchSecretSyncAdapters {
70
+ github?: typeof syncTreeseedGitHubEnvironment;
71
+ cloudflare?: typeof syncTreeseedCloudflareEnvironment;
72
+ railway?: typeof syncTreeseedRailwayEnvironment;
73
+ }
74
+ export interface ResolveProjectLaunchSecretValueOverlayOptions {
75
+ hostBindings?: Record<string, ProjectLaunchResolvedHostBinding> | null;
76
+ secretDeploymentPlan?: {
77
+ items?: ProjectLaunchSecretDeploymentPlanItem[] | null;
78
+ } | null;
79
+ valuesOverlay?: Record<string, string | undefined> | null;
80
+ valuesByScope?: Partial<Record<TreeseedConfigScope, Record<string, string | undefined> | null>> | null;
81
+ processEnv?: Record<string, string | undefined>;
82
+ scopes?: TreeseedConfigScope[];
83
+ }
84
+ export interface SyncProjectLaunchHostBindingSecretsOptions extends ResolveProjectLaunchSecretValueOverlayOptions {
85
+ projectRoot: string;
86
+ repository?: string | null;
87
+ dryRun?: boolean;
88
+ providers?: ProjectLaunchSecretSyncProvider[];
89
+ onProgress?: (event: ProjectLaunchSecretSyncProgressEvent) => void | Promise<void>;
90
+ adapters?: ProjectLaunchSecretSyncAdapters;
91
+ }
92
+ export declare class ProjectLaunchSecretSyncError extends Error {
93
+ readonly result: ProjectLaunchSecretSyncResult;
94
+ constructor(message: string, result: ProjectLaunchSecretSyncResult);
95
+ }
96
+ export declare function resolveProjectLaunchSecretValueOverlay(options: ResolveProjectLaunchSecretValueOverlayOptions): ProjectLaunchSecretValueOverlayResult;
97
+ export declare function syncProjectLaunchHostBindingSecrets(options: SyncProjectLaunchHostBindingSecretsOptions): Promise<ProjectLaunchSecretSyncResult>;