@treeseed/sdk 0.10.23 → 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 (49) 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/bootstrap-runner.d.ts +5 -1
  9. package/dist/operations/services/bootstrap-runner.js +34 -5
  10. package/dist/operations/services/config-runtime.d.ts +25 -9
  11. package/dist/operations/services/config-runtime.js +60 -12
  12. package/dist/operations/services/deploy.js +6 -1
  13. package/dist/operations/services/hub-launch.js +1 -0
  14. package/dist/operations/services/hub-provider-launch.d.ts +11 -1
  15. package/dist/operations/services/hub-provider-launch.js +81 -8
  16. package/dist/operations/services/project-host-operations.d.ts +153 -0
  17. package/dist/operations/services/project-host-operations.js +365 -0
  18. package/dist/operations/services/project-platform.d.ts +207 -177
  19. package/dist/operations/services/project-platform.js +96 -29
  20. package/dist/operations/services/railway-deploy.d.ts +33 -1
  21. package/dist/operations/services/railway-deploy.js +153 -44
  22. package/dist/operations/services/release-candidate.js +8 -2
  23. package/dist/operations/services/template-host-bindings.d.ts +68 -0
  24. package/dist/operations/services/template-host-bindings.js +400 -0
  25. package/dist/operations/services/template-registry.d.ts +22 -2
  26. package/dist/operations/services/template-registry.js +93 -6
  27. package/dist/operations/services/template-secret-sync.d.ts +97 -0
  28. package/dist/operations/services/template-secret-sync.js +292 -0
  29. package/dist/platform/contracts.d.ts +1 -0
  30. package/dist/platform/deploy-config.js +8 -1
  31. package/dist/platform/deploy-runtime.js +1 -0
  32. package/dist/platform/environment.d.ts +3 -0
  33. package/dist/project-workflow.d.ts +7 -1
  34. package/dist/reconcile/engine.d.ts +2 -0
  35. package/dist/reconcile/engine.js +58 -3
  36. package/dist/scripts/scaffold-site.js +3 -2
  37. package/dist/scripts/test-scaffold.js +2 -1
  38. package/dist/sdk-types.d.ts +87 -0
  39. package/dist/sdk-types.js +29 -0
  40. package/dist/template-catalog.js +3 -1
  41. package/dist/template-launch-requirements.d.ts +118 -0
  42. package/dist/template-launch-requirements.js +759 -0
  43. package/dist/template-launch-ui.d.ts +85 -0
  44. package/dist/template-launch-ui.js +189 -0
  45. package/dist/timing.d.ts +20 -0
  46. package/dist/timing.js +73 -0
  47. package/dist/treeseed/template-catalog/catalog.fixture.json +477 -0
  48. package/package.json +13 -1
  49. package/templates/github/deploy-web.workflow.yml +4 -0
@@ -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,
@@ -11,6 +15,7 @@ import {
11
15
  cliPackageVersion,
12
16
  agentPackageVersion,
13
17
  corePackageVersion,
18
+ cliPackageRoot,
14
19
  localTemplateArtifactsRoot,
15
20
  sdkPackageVersion
16
21
  } from "./runtime-paths.js";
@@ -37,10 +42,31 @@ function listFiles(root) {
37
42
  return files;
38
43
  }
39
44
  function listTemplateArtifactIds() {
40
- if (!existsSync(localTemplateArtifactsRoot)) {
41
- return [];
45
+ const packagedIds = existsSync(localTemplateArtifactsRoot) ? readdirSync(localTemplateArtifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
46
+ const localStarterIds = listLocalStarterArtifacts().map((entry) => entry.id);
47
+ return [.../* @__PURE__ */ new Set([...packagedIds, ...localStarterIds])].sort((left, right) => left.localeCompare(right, void 0, { sensitivity: "base" }));
48
+ }
49
+ const LOCAL_STARTER_ID_TO_DIRECTORY = {
50
+ "starter-research": "research",
51
+ "starter-engineering": "engineering",
52
+ "starter-information-hub": "information-hub"
53
+ };
54
+ function localStartersRoot() {
55
+ return resolve(cliPackageRoot, "..", "..", "starters");
56
+ }
57
+ function resolveLocalStarterArtifactRoot(id) {
58
+ const directory = LOCAL_STARTER_ID_TO_DIRECTORY[id];
59
+ if (!directory) {
60
+ return null;
42
61
  }
43
- return readdirSync(localTemplateArtifactsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right, void 0, { sensitivity: "base" }));
62
+ const artifactRoot = resolve(localStartersRoot(), directory);
63
+ return existsSync(resolve(artifactRoot, "template.config.json")) && existsSync(resolve(artifactRoot, "template")) ? artifactRoot : null;
64
+ }
65
+ function listLocalStarterArtifacts() {
66
+ return Object.keys(LOCAL_STARTER_ID_TO_DIRECTORY).map((id) => {
67
+ const artifactRoot = resolveLocalStarterArtifactRoot(id);
68
+ return artifactRoot ? { id, artifactRoot } : null;
69
+ }).filter((entry) => Boolean(entry));
44
70
  }
45
71
  function isTextFile(filePath) {
46
72
  return !/\.(png|jpe?g|gif|webp|ico|woff2?|ttf|eot|pdf|zip|gz)$/iu.test(filePath);
@@ -76,6 +102,7 @@ function validateTemplateManifest(definition) {
76
102
  if (!existsSync(templateRoot)) {
77
103
  throw new Error(`Template ${manifest.id} is missing template/ at ${templateRoot}.`);
78
104
  }
105
+ manifest.launchRequirements = normalizeTemplateLaunchRequirements(manifest.launchRequirements, `${manifestPath}: launchRequirements`);
79
106
  validateTemplatePlaceholders(definition);
80
107
  }
81
108
  function validateTemplatePlaceholders(definition) {
@@ -166,6 +193,14 @@ function materializeR2TemplateSource(product) {
166
193
  );
167
194
  }
168
195
  function resolveTemplateDefinitionPaths(product, options) {
196
+ const localStarterArtifactRoot = resolveLocalStarterArtifactRoot(product.id);
197
+ if (localStarterArtifactRoot) {
198
+ return {
199
+ artifactRoot: localStarterArtifactRoot,
200
+ manifestPath: resolve(localStarterArtifactRoot, "template.config.json"),
201
+ templateRoot: resolve(localStarterArtifactRoot, "template")
202
+ };
203
+ }
169
204
  if (existsSync(product.artifactManifestPath) && existsSync(product.templateRoot)) {
170
205
  return {
171
206
  artifactRoot: product.artifactRoot,
@@ -396,10 +431,52 @@ async function scaffoldTemplateProject(templateId, targetRoot, input, options =
396
431
  sourceRef: definition.product.fulfillment.source.ref,
397
432
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
398
433
  lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
399
- 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
+ } : {}
400
441
  });
401
442
  return definition.product;
402
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
+ }
403
480
  async function syncTemplateProject(siteRoot, options = {}) {
404
481
  const check = options.check === true;
405
482
  const state = loadTemplateState(siteRoot);
@@ -418,11 +495,19 @@ async function syncTemplateProject(siteRoot, options = {}) {
418
495
  }
419
496
  continue;
420
497
  }
421
- const nextContent = renderTemplateFile(sourcePath, state.replacements);
422
498
  const currentContent = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
499
+ const nextContent = preserveHostBindingOverlayIfNeeded(
500
+ relativePath,
501
+ currentContent,
502
+ renderTemplateFile(sourcePath, state.replacements),
503
+ state
504
+ );
423
505
  if (currentContent === nextContent) {
424
506
  continue;
425
507
  }
508
+ if (state.hostBindingPlans && structuredTemplateContentMatches(relativePath, currentContent, nextContent)) {
509
+ continue;
510
+ }
426
511
  if (!check) {
427
512
  ensureDir(targetPath);
428
513
  writeFileSync(targetPath, nextContent, "utf8");
@@ -462,7 +547,8 @@ function serializeTemplateRegistryEntry(product) {
462
547
  minCliVersion: product.minCliVersion,
463
548
  minCoreVersion: product.minCoreVersion,
464
549
  fulfillmentMode: product.fulfillment.mode ?? "packaged",
465
- source: product.fulfillment.source
550
+ source: product.fulfillment.source,
551
+ launchRequirements: product.launchRequirements
466
552
  };
467
553
  }
468
554
  async function exportTemplateCatalogYaml(options = {}) {
@@ -473,6 +559,7 @@ export {
473
559
  buildTemplateReplacements,
474
560
  exportTemplateCatalogYaml,
475
561
  listTemplateProducts,
562
+ recordTemplateHostBindingState,
476
563
  resolveTemplateDefinition,
477
564
  resolveTemplateProduct,
478
565
  scaffoldTemplateProject,