@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,365 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executePlatformRepositoryOperation
|
|
3
|
+
} from "../repository-operations.js";
|
|
4
|
+
import {
|
|
5
|
+
ProjectLaunchSecretSyncError,
|
|
6
|
+
syncProjectLaunchHostBindingSecrets
|
|
7
|
+
} from "./template-secret-sync.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveProjectLaunchHostBindings
|
|
10
|
+
} from "../../template-launch-requirements.js";
|
|
11
|
+
function requirementByKey(requirements) {
|
|
12
|
+
return new Map((requirements?.hosts ?? []).map((requirement) => [requirement.key, requirement]));
|
|
13
|
+
}
|
|
14
|
+
function bindingMode(binding) {
|
|
15
|
+
if (!binding) return null;
|
|
16
|
+
if (binding.managedHostKey || binding.host?.ownership === "treeseed_managed" || binding.provenance.selectedBy === "managed-default") return "treeseed_managed";
|
|
17
|
+
if (binding.hostId || binding.host?.id) return "team_owned";
|
|
18
|
+
return "none";
|
|
19
|
+
}
|
|
20
|
+
function inputFromResolved(binding) {
|
|
21
|
+
return {
|
|
22
|
+
requirementKey: binding.requirementKey,
|
|
23
|
+
requirementKind: binding.requirementKind,
|
|
24
|
+
type: binding.type,
|
|
25
|
+
provider: binding.provider,
|
|
26
|
+
hostId: binding.hostId ?? binding.host?.id ?? null,
|
|
27
|
+
managedHostKey: binding.managedHostKey ?? null,
|
|
28
|
+
mode: bindingMode(binding),
|
|
29
|
+
displayName: binding.displayName,
|
|
30
|
+
environmentScopes: binding.environmentScopes,
|
|
31
|
+
configValues: binding.configValues,
|
|
32
|
+
environmentValues: binding.environmentValues,
|
|
33
|
+
secretRefs: binding.secretRefs,
|
|
34
|
+
selectedBy: binding.provenance.selectedBy
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function inventoryFromResolved(binding) {
|
|
38
|
+
const id = binding.host?.id ?? binding.hostId ?? binding.managedHostKey ?? null;
|
|
39
|
+
if (!id) return null;
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
type: binding.type,
|
|
43
|
+
provider: binding.provider,
|
|
44
|
+
ownership: binding.host?.ownership ?? (binding.managedHostKey ? "treeseed_managed" : null),
|
|
45
|
+
name: binding.host?.name ?? binding.displayName,
|
|
46
|
+
accountLabel: binding.host?.accountLabel ?? null,
|
|
47
|
+
organizationOrOwner: binding.host?.organizationOrOwner ?? null,
|
|
48
|
+
allowedEnvironments: binding.environmentScopes,
|
|
49
|
+
status: binding.host?.status ?? "active",
|
|
50
|
+
metadata: {
|
|
51
|
+
...binding.host?.metadata ?? {},
|
|
52
|
+
hostType: binding.type
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function mergeInventory(currentBindings, input, type) {
|
|
57
|
+
const records = [...input ?? []];
|
|
58
|
+
const seen = new Set(records.map((record) => record.id));
|
|
59
|
+
for (const binding of Object.values(currentBindings)) {
|
|
60
|
+
if (binding.type !== type) continue;
|
|
61
|
+
const record = inventoryFromResolved(binding);
|
|
62
|
+
if (record && !seen.has(record.id)) {
|
|
63
|
+
records.push(record);
|
|
64
|
+
seen.add(record.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return records;
|
|
68
|
+
}
|
|
69
|
+
function mergeTeamHostInventory(currentBindings, input) {
|
|
70
|
+
const records = [...input ?? []];
|
|
71
|
+
const seen = new Set(records.map((record) => record.id));
|
|
72
|
+
for (const binding of Object.values(currentBindings)) {
|
|
73
|
+
if (!["web", "email", "ai"].includes(binding.type)) continue;
|
|
74
|
+
const record = inventoryFromResolved(binding);
|
|
75
|
+
if (record && !seen.has(record.id)) {
|
|
76
|
+
records.push(record);
|
|
77
|
+
seen.add(record.id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return records;
|
|
81
|
+
}
|
|
82
|
+
function hostRequirementInputSet(currentHostBindings, replacementHostBindings) {
|
|
83
|
+
const inputs = {};
|
|
84
|
+
for (const [key, binding] of Object.entries(currentHostBindings)) {
|
|
85
|
+
inputs[key] = inputFromResolved(binding);
|
|
86
|
+
}
|
|
87
|
+
for (const [key, binding] of Object.entries(replacementHostBindings ?? {})) {
|
|
88
|
+
inputs[key] = binding;
|
|
89
|
+
}
|
|
90
|
+
return inputs;
|
|
91
|
+
}
|
|
92
|
+
function bindingChanged(previous, next) {
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
provider: previous?.provider ?? null,
|
|
95
|
+
hostId: previous?.hostId ?? previous?.host?.id ?? null,
|
|
96
|
+
managedHostKey: previous?.managedHostKey ?? null,
|
|
97
|
+
mode: bindingMode(previous)
|
|
98
|
+
}) !== JSON.stringify({
|
|
99
|
+
provider: next?.provider ?? null,
|
|
100
|
+
hostId: next?.hostId ?? next?.host?.id ?? null,
|
|
101
|
+
managedHostKey: next?.managedHostKey ?? null,
|
|
102
|
+
mode: bindingMode(next)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function requirementDiagnostics(requirement, binding) {
|
|
106
|
+
const diagnostics = [];
|
|
107
|
+
const hostId = binding?.hostId ?? binding?.host?.id ?? binding?.managedHostKey ?? null;
|
|
108
|
+
if (!binding || bindingMode(binding) === "none") {
|
|
109
|
+
if (requirement.required) {
|
|
110
|
+
diagnostics.push({
|
|
111
|
+
code: "missing_required_host",
|
|
112
|
+
status: "blocked",
|
|
113
|
+
message: `${requirement.displayName} is required and has no selected host.`,
|
|
114
|
+
requirementKey: requirement.key
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return diagnostics;
|
|
118
|
+
}
|
|
119
|
+
if (binding.type !== requirement.type) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
code: "incompatible_host_type",
|
|
122
|
+
status: "blocked",
|
|
123
|
+
message: `${requirement.displayName} requires ${requirement.type} hosts, but ${binding.type} is selected.`,
|
|
124
|
+
requirementKey: requirement.key,
|
|
125
|
+
provider: binding.provider,
|
|
126
|
+
hostId
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (requirement.compatibleProviders?.length && !requirement.compatibleProviders.includes(binding.provider)) {
|
|
130
|
+
diagnostics.push({
|
|
131
|
+
code: "incompatible_provider",
|
|
132
|
+
status: "blocked",
|
|
133
|
+
message: `${requirement.displayName} requires ${requirement.compatibleProviders.join(", ")} provider support.`,
|
|
134
|
+
requirementKey: requirement.key,
|
|
135
|
+
provider: binding.provider,
|
|
136
|
+
hostId
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const status = String(binding.host?.status ?? "").trim();
|
|
140
|
+
if (status && !["active", "ready"].includes(status)) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
code: "host_not_ready",
|
|
143
|
+
status: requirement.required ? "blocked" : "warning",
|
|
144
|
+
message: `${requirement.displayName} host is ${status}.`,
|
|
145
|
+
requirementKey: requirement.key,
|
|
146
|
+
provider: binding.provider,
|
|
147
|
+
hostId
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return diagnostics;
|
|
151
|
+
}
|
|
152
|
+
function worstStatus(diagnostics) {
|
|
153
|
+
if (diagnostics.some((diagnostic) => diagnostic.status === "blocked")) return "blocked";
|
|
154
|
+
if (diagnostics.some((diagnostic) => diagnostic.status === "warning")) return "warning";
|
|
155
|
+
return "ok";
|
|
156
|
+
}
|
|
157
|
+
function deriveProjectHostBindingsView(options) {
|
|
158
|
+
const bindings = options.hostBindings ?? {};
|
|
159
|
+
const configWrites = options.hostBindingPlans?.configWrites ?? [];
|
|
160
|
+
const secretItems = options.hostBindingPlans?.secretDeployment?.items ?? [];
|
|
161
|
+
const requirements = (options.launchRequirements?.hosts ?? []).map((requirement) => {
|
|
162
|
+
const binding = bindings[requirement.key];
|
|
163
|
+
const diagnostics2 = requirementDiagnostics(requirement, binding);
|
|
164
|
+
const marketHostId = binding?.hostId ?? binding?.host?.id ?? binding?.managedHostKey ?? null;
|
|
165
|
+
const scopedConfigWrites = configWrites.filter((write) => write.requirementKey === requirement.key);
|
|
166
|
+
const scopedSecretItems = secretItems.filter((item) => item.requirementKey === requirement.key);
|
|
167
|
+
return {
|
|
168
|
+
requirementKey: requirement.key,
|
|
169
|
+
displayName: requirement.displayName,
|
|
170
|
+
type: requirement.type,
|
|
171
|
+
required: requirement.required,
|
|
172
|
+
purpose: requirement.purpose,
|
|
173
|
+
compatibleProviders: requirement.compatibleProviders ?? [],
|
|
174
|
+
binding: binding ? {
|
|
175
|
+
provider: binding.provider,
|
|
176
|
+
hostId: binding.hostId ?? binding.host?.id ?? null,
|
|
177
|
+
managedHostKey: binding.managedHostKey ?? null,
|
|
178
|
+
mode: bindingMode(binding),
|
|
179
|
+
displayName: binding.displayName,
|
|
180
|
+
ownership: binding.host?.ownership ?? null,
|
|
181
|
+
status: binding.host?.status ?? null,
|
|
182
|
+
environmentScopes: binding.environmentScopes,
|
|
183
|
+
selectedBy: binding.provenance.selectedBy,
|
|
184
|
+
selectedAt: binding.provenance.selectedAt
|
|
185
|
+
} : null,
|
|
186
|
+
configWrites: scopedConfigWrites.map((write) => ({
|
|
187
|
+
target: write.target,
|
|
188
|
+
path: write.path,
|
|
189
|
+
valueFrom: write.valueFrom,
|
|
190
|
+
provider: write.provider ?? binding?.provider ?? null
|
|
191
|
+
})),
|
|
192
|
+
secretTargets: scopedSecretItems.map((item) => ({
|
|
193
|
+
env: item.env,
|
|
194
|
+
targets: item.targets,
|
|
195
|
+
scopes: item.scopes,
|
|
196
|
+
sensitivity: item.sensitivity,
|
|
197
|
+
provider: binding?.provider ?? null
|
|
198
|
+
})),
|
|
199
|
+
audit: {
|
|
200
|
+
status: worstStatus(diagnostics2),
|
|
201
|
+
diagnostics: diagnostics2,
|
|
202
|
+
marketHostId,
|
|
203
|
+
repositoryConfig: scopedConfigWrites.length > 0 ? "planned" : "not_declared"
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
const diagnostics = requirements.flatMap((requirement) => requirement.audit.diagnostics);
|
|
208
|
+
return {
|
|
209
|
+
requirements,
|
|
210
|
+
summary: {
|
|
211
|
+
status: worstStatus(diagnostics),
|
|
212
|
+
total: requirements.length,
|
|
213
|
+
blocked: diagnostics.filter((diagnostic) => diagnostic.status === "blocked").length,
|
|
214
|
+
warnings: diagnostics.filter((diagnostic) => diagnostic.status === "warning").length
|
|
215
|
+
},
|
|
216
|
+
diagnostics
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function planProjectHostBindingOperation(options) {
|
|
220
|
+
const current = options.currentHostBindings ?? {};
|
|
221
|
+
const replacements = options.replacementHostBindings ?? {};
|
|
222
|
+
const requirementsByKey = requirementByKey(options.launchRequirements);
|
|
223
|
+
const requirementKey = options.requirementKey ?? Object.keys(replacements)[0] ?? null;
|
|
224
|
+
if (requirementKey && !requirementsByKey.has(requirementKey)) {
|
|
225
|
+
throw new Error(`Unknown launch host requirement "${requirementKey}".`);
|
|
226
|
+
}
|
|
227
|
+
const inputs = hostRequirementInputSet(current, replacements);
|
|
228
|
+
const resolved = resolveProjectLaunchHostBindings({
|
|
229
|
+
hostBindings: inputs,
|
|
230
|
+
launchRequirements: options.launchRequirements,
|
|
231
|
+
repositoryHosts: mergeInventory(current, options.repositoryHosts, "repository"),
|
|
232
|
+
teamHosts: mergeTeamHostInventory(current, options.teamHosts),
|
|
233
|
+
managedHosts: options.managedHosts,
|
|
234
|
+
defaultHosts: options.defaultHosts,
|
|
235
|
+
projectSlug: options.projectSlug,
|
|
236
|
+
projectName: options.projectName,
|
|
237
|
+
standardProjectLaunch: true,
|
|
238
|
+
selectedAt: options.selectedAt
|
|
239
|
+
});
|
|
240
|
+
const changedRequirementKeys = [.../* @__PURE__ */ new Set([
|
|
241
|
+
...Object.keys(current),
|
|
242
|
+
...Object.keys(resolved.hostBindings)
|
|
243
|
+
])].filter((key) => bindingChanged(current[key], resolved.hostBindings[key]));
|
|
244
|
+
const hostBindingPlans = {
|
|
245
|
+
configWrites: resolved.configWritePlan,
|
|
246
|
+
secretDeployment: resolved.secretDeploymentPlan
|
|
247
|
+
};
|
|
248
|
+
const audit = deriveProjectHostBindingsView({
|
|
249
|
+
launchRequirements: options.launchRequirements,
|
|
250
|
+
hostBindings: resolved.hostBindings,
|
|
251
|
+
hostBindingPlans
|
|
252
|
+
});
|
|
253
|
+
const scopedKeys = requirementKey ? [requirementKey] : changedRequirementKeys;
|
|
254
|
+
return {
|
|
255
|
+
kind: options.kind,
|
|
256
|
+
requirementKey,
|
|
257
|
+
previousHostBindings: current,
|
|
258
|
+
nextHostBindings: resolved.hostBindings,
|
|
259
|
+
compatibility: resolved.compatibility,
|
|
260
|
+
hostBindingPlans,
|
|
261
|
+
audit,
|
|
262
|
+
operationSummary: {
|
|
263
|
+
requiresRepositoryConfigWrite: resolved.configWritePlan.some((write) => scopedKeys.length === 0 || scopedKeys.includes(write.requirementKey)),
|
|
264
|
+
requiresSecretSync: (resolved.secretDeploymentPlan.items ?? []).some((item) => scopedKeys.length === 0 || scopedKeys.includes(item.requirementKey)),
|
|
265
|
+
changedRequirementKeys
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function scopedRequirementKeys(input) {
|
|
270
|
+
if (input.requirementKey) return [input.requirementKey];
|
|
271
|
+
if (input.kind === "replace") return input.operationSummary?.changedRequirementKeys ?? [];
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
function scopedPlans(input) {
|
|
275
|
+
const keys = scopedRequirementKeys(input);
|
|
276
|
+
if (keys.length === 0) return input.hostBindingPlans;
|
|
277
|
+
const keySet = new Set(keys);
|
|
278
|
+
return {
|
|
279
|
+
configWrites: input.hostBindingPlans.configWrites.filter((write) => keySet.has(write.requirementKey)),
|
|
280
|
+
secretDeployment: {
|
|
281
|
+
items: (input.hostBindingPlans.secretDeployment.items ?? []).filter((item) => keySet.has(item.requirementKey))
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function repositorySlug(result) {
|
|
286
|
+
return result.repository.owner ? `${result.repository.owner}/${result.repository.name}` : result.repository.name;
|
|
287
|
+
}
|
|
288
|
+
async function executeProjectHostBindingOperation(input, context) {
|
|
289
|
+
const plans = scopedPlans(input);
|
|
290
|
+
const requiresRepositoryConfigWrite = input.kind === "replace" && plans.configWrites.length > 0;
|
|
291
|
+
const repositoryOperation = requiresRepositoryConfigWrite ? "apply_host_binding_config" : "audit_host_binding_config";
|
|
292
|
+
const repositoryResult = await executePlatformRepositoryOperation(repositoryOperation, {
|
|
293
|
+
projectId: input.projectId ?? void 0,
|
|
294
|
+
teamId: input.teamId ?? void 0,
|
|
295
|
+
repository: input.repository,
|
|
296
|
+
hostBindings: input.hostBindings,
|
|
297
|
+
hostBindingPlans: plans,
|
|
298
|
+
launchInput: {
|
|
299
|
+
projectSlug: input.projectSlug ?? null,
|
|
300
|
+
projectName: input.projectName ?? null,
|
|
301
|
+
repoName: input.repositoryName ?? input.projectSlug ?? null
|
|
302
|
+
},
|
|
303
|
+
derived: {
|
|
304
|
+
projectSlug: input.projectSlug ?? null,
|
|
305
|
+
projectName: input.projectName ?? null,
|
|
306
|
+
repositoryName: input.repositoryName ?? input.projectSlug ?? null
|
|
307
|
+
},
|
|
308
|
+
commitMessage: input.commitMessage ?? void 0,
|
|
309
|
+
approvalRequired: input.approvalRequired,
|
|
310
|
+
approvalId: input.approvalId ?? void 0
|
|
311
|
+
}, {
|
|
312
|
+
workspaceRoot: context.workspaceRoot,
|
|
313
|
+
environment: context.environment
|
|
314
|
+
});
|
|
315
|
+
let secretSync = null;
|
|
316
|
+
const requiresSecretSync = ["replace", "resync", "rotate"].includes(input.kind) && (plans.secretDeployment.items ?? []).length > 0;
|
|
317
|
+
if (requiresSecretSync) {
|
|
318
|
+
try {
|
|
319
|
+
secretSync = await syncProjectLaunchHostBindingSecrets({
|
|
320
|
+
projectRoot: repositoryResult.repositoryPath,
|
|
321
|
+
repository: repositorySlug(repositoryResult),
|
|
322
|
+
hostBindings: input.hostBindings,
|
|
323
|
+
secretDeploymentPlan: plans.secretDeployment,
|
|
324
|
+
valuesOverlay: context.valuesOverlay,
|
|
325
|
+
valuesByScope: context.valuesByScope,
|
|
326
|
+
processEnv: context.processEnv,
|
|
327
|
+
dryRun: input.dryRun,
|
|
328
|
+
onProgress: context.onProgress
|
|
329
|
+
});
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof ProjectLaunchSecretSyncError) {
|
|
332
|
+
secretSync = error.result;
|
|
333
|
+
} else {
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
ok: secretSync ? secretSync.ok : true,
|
|
340
|
+
kind: input.kind,
|
|
341
|
+
requirementKey: input.requirementKey ?? null,
|
|
342
|
+
hostBindings: input.hostBindings,
|
|
343
|
+
previousHostBindings: input.previousHostBindings ?? {},
|
|
344
|
+
hostBindingPlans: plans,
|
|
345
|
+
repository: {
|
|
346
|
+
operation: repositoryOperation,
|
|
347
|
+
branch: repositoryResult.operationBranch ?? repositoryResult.branch,
|
|
348
|
+
commitSha: repositoryResult.commitSha,
|
|
349
|
+
changedPaths: repositoryResult.changedPaths,
|
|
350
|
+
audit: repositoryResult.output.hostBindingAudit ?? null,
|
|
351
|
+
config: repositoryResult.output.hostBindingConfig ?? null
|
|
352
|
+
},
|
|
353
|
+
secretSync,
|
|
354
|
+
summary: {
|
|
355
|
+
requiresRepositoryConfigWrite,
|
|
356
|
+
requiresSecretSync,
|
|
357
|
+
changedRequirementKeys: input.operationSummary?.changedRequirementKeys ?? []
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export {
|
|
362
|
+
deriveProjectHostBindingsView,
|
|
363
|
+
executeProjectHostBindingOperation,
|
|
364
|
+
planProjectHostBindingOperation
|
|
365
|
+
};
|