@treeseed/sdk 0.10.24 → 0.10.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/config-runtime.d.ts +24 -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 +198 -193
- package/dist/operations/services/project-platform.js +29 -14
- package/dist/operations/services/railway-deploy.d.ts +3 -0
- package/dist/operations/services/railway-deploy.js +74 -35
- 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 +60 -3
- package/dist/operations/services/template-secret-sync.d.ts +97 -0
- package/dist/operations/services/template-secret-sync.js +292 -0
- package/dist/platform/environment.d.ts +3 -0
- package/dist/project-workflow.d.ts +7 -1
- 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/treeseed/template-catalog/catalog.fixture.json +330 -3
- 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>;
|