@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.
- package/dist/index.d.ts +12 -2
- package/dist/index.js +42 -1
- package/dist/market-client.d.ts +23 -0
- package/dist/market-client.js +30 -0
- package/dist/operations/providers/default.js +103 -10
- package/dist/operations/repository-operations.d.ts +6 -1
- package/dist/operations/repository-operations.js +44 -0
- package/dist/operations/services/bootstrap-runner.d.ts +5 -1
- package/dist/operations/services/bootstrap-runner.js +34 -5
- package/dist/operations/services/config-runtime.d.ts +25 -9
- package/dist/operations/services/config-runtime.js +60 -12
- package/dist/operations/services/deploy.js +6 -1
- package/dist/operations/services/hub-launch.js +1 -0
- package/dist/operations/services/hub-provider-launch.d.ts +11 -1
- package/dist/operations/services/hub-provider-launch.js +81 -8
- package/dist/operations/services/project-host-operations.d.ts +153 -0
- package/dist/operations/services/project-host-operations.js +365 -0
- package/dist/operations/services/project-platform.d.ts +207 -177
- package/dist/operations/services/project-platform.js +96 -29
- package/dist/operations/services/railway-deploy.d.ts +33 -1
- package/dist/operations/services/railway-deploy.js +153 -44
- package/dist/operations/services/release-candidate.js +8 -2
- package/dist/operations/services/template-host-bindings.d.ts +68 -0
- package/dist/operations/services/template-host-bindings.js +400 -0
- package/dist/operations/services/template-registry.d.ts +22 -2
- package/dist/operations/services/template-registry.js +93 -6
- package/dist/operations/services/template-secret-sync.d.ts +97 -0
- package/dist/operations/services/template-secret-sync.js +292 -0
- package/dist/platform/contracts.d.ts +1 -0
- package/dist/platform/deploy-config.js +8 -1
- package/dist/platform/deploy-runtime.js +1 -0
- package/dist/platform/environment.d.ts +3 -0
- package/dist/project-workflow.d.ts +7 -1
- package/dist/reconcile/engine.d.ts +2 -0
- package/dist/reconcile/engine.js +58 -3
- package/dist/scripts/scaffold-site.js +3 -2
- package/dist/scripts/test-scaffold.js +2 -1
- package/dist/sdk-types.d.ts +87 -0
- package/dist/sdk-types.js +29 -0
- package/dist/template-catalog.js +3 -1
- package/dist/template-launch-requirements.d.ts +118 -0
- package/dist/template-launch-requirements.js +759 -0
- package/dist/template-launch-ui.d.ts +85 -0
- package/dist/template-launch-ui.js +189 -0
- package/dist/timing.d.ts +20 -0
- package/dist/timing.js +73 -0
- package/dist/treeseed/template-catalog/catalog.fixture.json +477 -0
- package/package.json +13 -1
- 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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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,
|