@treeseed/sdk 0.8.12 → 0.8.14
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/control-plane-client.d.ts +47 -2
- package/dist/control-plane-client.js +142 -0
- package/dist/d1-store.d.ts +19 -2
- package/dist/d1-store.js +110 -0
- package/dist/graph/context-query-contracts.d.ts +1 -0
- package/dist/graph/context-query-contracts.js +5 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/market-client.d.ts +7 -0
- package/dist/market-client.js +22 -0
- package/dist/model-registry.js +74 -0
- package/dist/operations/providers/default.js +6 -0
- package/dist/operations/services/deploy.js +8 -8
- package/dist/operations-registry.js +1 -0
- package/dist/scripts/tenant-d1-migrate-local.js +1 -0
- package/dist/sdk-types.d.ts +30 -3
- package/dist/sdk-types.js +5 -1
- package/dist/sdk.d.ts +6 -1
- package/dist/sdk.js +20 -0
- package/dist/seeds/errors.d.ts +5 -0
- package/dist/seeds/errors.js +22 -0
- package/dist/seeds/index.d.ts +20 -0
- package/dist/seeds/index.js +53 -0
- package/dist/seeds/loader.d.ts +8 -0
- package/dist/seeds/loader.js +32 -0
- package/dist/seeds/normalize.d.ts +13 -0
- package/dist/seeds/normalize.js +275 -0
- package/dist/seeds/planner.d.ts +10 -0
- package/dist/seeds/planner.js +111 -0
- package/dist/seeds/schema.d.ts +2 -0
- package/dist/seeds/schema.js +544 -0
- package/dist/seeds/types.d.ts +220 -0
- package/dist/seeds/types.js +4 -0
- package/dist/stores/operational-store.d.ts +11 -1
- package/dist/stores/operational-store.js +244 -0
- package/package.json +5 -1
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { errorDiagnostic, warningDiagnostic } from "./errors.js";
|
|
2
|
+
import {
|
|
3
|
+
SEED_ENVIRONMENTS
|
|
4
|
+
} from "./types.js";
|
|
5
|
+
const RESOURCE_BUCKETS = [
|
|
6
|
+
"teams",
|
|
7
|
+
"repositoryHosts",
|
|
8
|
+
"projects",
|
|
9
|
+
"hubRepositories",
|
|
10
|
+
"products",
|
|
11
|
+
"catalogArtifacts",
|
|
12
|
+
"capacityProviders",
|
|
13
|
+
"capacityGrants",
|
|
14
|
+
"workPolicies",
|
|
15
|
+
"agentPools"
|
|
16
|
+
];
|
|
17
|
+
const SUPPORTED_BUCKETS = /* @__PURE__ */ new Set(["teams", "repositoryHosts", "projects", "hubRepositories", "products", "catalogArtifacts", "capacityProviders", "capacityGrants", "workPolicies"]);
|
|
18
|
+
const ALLOWED_ENVIRONMENTS = new Set(SEED_ENVIRONMENTS);
|
|
19
|
+
const CREDENTIAL_REF_PATTERN = /^(?:env|secret|provider-session):[A-Za-z0-9_./:-]+$/u;
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function asString(value) {
|
|
24
|
+
return typeof value === "string" ? value.trim() : "";
|
|
25
|
+
}
|
|
26
|
+
function parseEnvironments(value, path, diagnostics) {
|
|
27
|
+
if (value === void 0) return void 0;
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
diagnostics.push(errorDiagnostic("seed.invalid_environments", "Expected an array of environments.", path));
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const result = [];
|
|
33
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
34
|
+
const environment = asString(value[index]);
|
|
35
|
+
if (!ALLOWED_ENVIRONMENTS.has(environment)) {
|
|
36
|
+
diagnostics.push(errorDiagnostic("seed.unknown_environment", `Unknown environment: ${environment || String(value[index])}.`, `${path}[${index}]`));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!result.includes(environment)) {
|
|
40
|
+
result.push(environment);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
function requireString(record, field, path, diagnostics) {
|
|
46
|
+
const value = asString(record[field]);
|
|
47
|
+
if (!value) {
|
|
48
|
+
diagnostics.push(errorDiagnostic("seed.missing_field", `Missing required field: ${field}.`, `${path}.${field}`));
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function numberField(record, field, path, diagnostics) {
|
|
53
|
+
const value = record[field];
|
|
54
|
+
if (value === void 0) return void 0;
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
56
|
+
diagnostics.push(errorDiagnostic("seed.invalid_number", `Expected ${field} to be a finite number.`, `${path}.${field}`));
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function objectField(record, field, path, diagnostics) {
|
|
62
|
+
const value = record[field];
|
|
63
|
+
if (value === void 0) return void 0;
|
|
64
|
+
if (!isRecord(value)) {
|
|
65
|
+
diagnostics.push(errorDiagnostic("seed.invalid_object", `Expected ${field} to be an object.`, `${path}.${field}`));
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
function stringArrayField(record, field, path, diagnostics) {
|
|
71
|
+
const value = record[field];
|
|
72
|
+
if (value === void 0) return void 0;
|
|
73
|
+
if (!Array.isArray(value)) {
|
|
74
|
+
diagnostics.push(errorDiagnostic("seed.invalid_array", `Expected ${field} to be an array.`, `${path}.${field}`));
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
return value.map((entry) => asString(entry)).filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
function keyBase(record, path, diagnostics) {
|
|
80
|
+
const key = requireString(record, "key", path, diagnostics);
|
|
81
|
+
const environments = parseEnvironments(record.environments, `${path}.environments`, diagnostics);
|
|
82
|
+
return { key, ...environments ? { environments } : {} };
|
|
83
|
+
}
|
|
84
|
+
function parseTeam(value, path, diagnostics) {
|
|
85
|
+
if (!isRecord(value)) {
|
|
86
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected team resource to be an object.", path));
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
...keyBase(value, path, diagnostics),
|
|
91
|
+
slug: requireString(value, "slug", path, diagnostics),
|
|
92
|
+
name: asString(value.name) || void 0,
|
|
93
|
+
displayName: asString(value.displayName) || void 0,
|
|
94
|
+
logoUrl: asString(value.logoUrl) || void 0,
|
|
95
|
+
profileSummary: asString(value.profileSummary) || void 0,
|
|
96
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseRepository(value, path, diagnostics) {
|
|
100
|
+
if (!isRecord(value)) {
|
|
101
|
+
diagnostics.push(errorDiagnostic("seed.invalid_repository", "Expected repository to be an object.", path));
|
|
102
|
+
return { role: "", provider: "", owner: "", name: "", gitUrl: "" };
|
|
103
|
+
}
|
|
104
|
+
const repository = {
|
|
105
|
+
role: requireString(value, "role", path, diagnostics),
|
|
106
|
+
provider: requireString(value, "provider", path, diagnostics),
|
|
107
|
+
owner: requireString(value, "owner", path, diagnostics),
|
|
108
|
+
name: requireString(value, "name", path, diagnostics),
|
|
109
|
+
gitUrl: requireString(value, "gitUrl", path, diagnostics),
|
|
110
|
+
defaultBranch: asString(value.defaultBranch) || void 0,
|
|
111
|
+
checkoutPath: asString(value.checkoutPath) || void 0,
|
|
112
|
+
submodulePath: asString(value.submodulePath) || void 0,
|
|
113
|
+
webUrl: asString(value.webUrl) || void 0
|
|
114
|
+
};
|
|
115
|
+
validateRepository(repository, path, diagnostics);
|
|
116
|
+
return repository;
|
|
117
|
+
}
|
|
118
|
+
function parseProject(value, path, diagnostics) {
|
|
119
|
+
if (!isRecord(value)) {
|
|
120
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected project resource to be an object.", path));
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...keyBase(value, path, diagnostics),
|
|
125
|
+
team: requireString(value, "team", path, diagnostics),
|
|
126
|
+
slug: requireString(value, "slug", path, diagnostics),
|
|
127
|
+
name: requireString(value, "name", path, diagnostics),
|
|
128
|
+
description: asString(value.description) || void 0,
|
|
129
|
+
kind: asString(value.kind) || void 0,
|
|
130
|
+
repository: parseRepository(value.repository, `${path}.repository`, diagnostics),
|
|
131
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseRepositoryHost(value, path, diagnostics) {
|
|
135
|
+
if (!isRecord(value)) {
|
|
136
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected repository host resource to be an object.", path));
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const credentialRef = asString(value.credentialRef) || void 0;
|
|
140
|
+
if (credentialRef && !CREDENTIAL_REF_PATTERN.test(credentialRef)) {
|
|
141
|
+
diagnostics.push(errorDiagnostic("seed.invalid_credential_ref", "Repository host credentialRef must be env:, secret:, or provider-session:.", `${path}.credentialRef`));
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
...keyBase(value, path, diagnostics),
|
|
145
|
+
team: requireString(value, "team", path, diagnostics),
|
|
146
|
+
provider: requireString(value, "provider", path, diagnostics),
|
|
147
|
+
name: requireString(value, "name", path, diagnostics),
|
|
148
|
+
ownership: asString(value.ownership) || void 0,
|
|
149
|
+
accountLabel: asString(value.accountLabel) || void 0,
|
|
150
|
+
organizationOrOwner: requireString(value, "organizationOrOwner", path, diagnostics),
|
|
151
|
+
defaultVisibility: asString(value.defaultVisibility) || void 0,
|
|
152
|
+
softwareRepositoryNameTemplate: asString(value.softwareRepositoryNameTemplate) || void 0,
|
|
153
|
+
contentRepositoryNameTemplate: asString(value.contentRepositoryNameTemplate) || void 0,
|
|
154
|
+
branchPolicy: objectField(value, "branchPolicy", path, diagnostics),
|
|
155
|
+
workflowPolicy: objectField(value, "workflowPolicy", path, diagnostics),
|
|
156
|
+
allowedProjectKinds: stringArrayField(value, "allowedProjectKinds", path, diagnostics),
|
|
157
|
+
status: asString(value.status) || void 0,
|
|
158
|
+
credentialRef,
|
|
159
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function parseHubRepository(value, path, diagnostics) {
|
|
163
|
+
if (!isRecord(value)) {
|
|
164
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected hub repository resource to be an object.", path));
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const repository = {
|
|
168
|
+
...keyBase(value, path, diagnostics),
|
|
169
|
+
project: requireString(value, "project", path, diagnostics),
|
|
170
|
+
role: requireString(value, "role", path, diagnostics),
|
|
171
|
+
repositoryHost: asString(value.repositoryHost) || void 0,
|
|
172
|
+
provider: requireString(value, "provider", path, diagnostics),
|
|
173
|
+
owner: requireString(value, "owner", path, diagnostics),
|
|
174
|
+
name: requireString(value, "name", path, diagnostics),
|
|
175
|
+
gitUrl: requireString(value, "gitUrl", path, diagnostics),
|
|
176
|
+
defaultBranch: asString(value.defaultBranch) || void 0,
|
|
177
|
+
currentBranch: asString(value.currentBranch) || void 0,
|
|
178
|
+
submodulePath: asString(value.submodulePath) || void 0,
|
|
179
|
+
status: asString(value.status) || void 0,
|
|
180
|
+
accessPolicy: objectField(value, "accessPolicy", path, diagnostics),
|
|
181
|
+
releasePolicy: objectField(value, "releasePolicy", path, diagnostics),
|
|
182
|
+
publishPolicy: objectField(value, "publishPolicy", path, diagnostics),
|
|
183
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
184
|
+
};
|
|
185
|
+
validateRepository({
|
|
186
|
+
role: repository.role,
|
|
187
|
+
provider: repository.provider,
|
|
188
|
+
owner: repository.owner,
|
|
189
|
+
name: repository.name,
|
|
190
|
+
gitUrl: repository.gitUrl,
|
|
191
|
+
defaultBranch: repository.defaultBranch,
|
|
192
|
+
submodulePath: repository.submodulePath
|
|
193
|
+
}, path, diagnostics);
|
|
194
|
+
return repository;
|
|
195
|
+
}
|
|
196
|
+
function parseProduct(value, path, diagnostics) {
|
|
197
|
+
if (!isRecord(value)) {
|
|
198
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected product resource to be an object.", path));
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
...keyBase(value, path, diagnostics),
|
|
203
|
+
team: requireString(value, "team", path, diagnostics),
|
|
204
|
+
kind: requireString(value, "kind", path, diagnostics),
|
|
205
|
+
slug: requireString(value, "slug", path, diagnostics),
|
|
206
|
+
title: requireString(value, "title", path, diagnostics),
|
|
207
|
+
summary: asString(value.summary) || void 0,
|
|
208
|
+
visibility: asString(value.visibility) || void 0,
|
|
209
|
+
listingEnabled: typeof value.listingEnabled === "boolean" ? value.listingEnabled : void 0,
|
|
210
|
+
offerMode: asString(value.offerMode) || void 0,
|
|
211
|
+
manifestKey: asString(value.manifestKey) || void 0,
|
|
212
|
+
artifactKey: asString(value.artifactKey) || void 0,
|
|
213
|
+
searchText: asString(value.searchText) || void 0,
|
|
214
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function parseCatalogArtifact(value, path, diagnostics) {
|
|
218
|
+
if (!isRecord(value)) {
|
|
219
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected catalog artifact resource to be an object.", path));
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (value.content !== void 0 || value.bytes !== void 0 || value.data !== void 0) {
|
|
223
|
+
diagnostics.push(errorDiagnostic("seed.inline_artifact_content", "Catalog artifact resources must reference content keys, not inline bytes/content.", path));
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
...keyBase(value, path, diagnostics),
|
|
227
|
+
product: requireString(value, "product", path, diagnostics),
|
|
228
|
+
version: requireString(value, "version", path, diagnostics),
|
|
229
|
+
kind: requireString(value, "kind", path, diagnostics),
|
|
230
|
+
contentKey: requireString(value, "contentKey", path, diagnostics),
|
|
231
|
+
manifestKey: asString(value.manifestKey) || void 0,
|
|
232
|
+
publishedAt: asString(value.publishedAt) || void 0,
|
|
233
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseLane(value, path, diagnostics) {
|
|
237
|
+
if (!isRecord(value)) {
|
|
238
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected lane resource to be an object.", path));
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
...keyBase(value, path, diagnostics),
|
|
243
|
+
name: requireString(value, "name", path, diagnostics),
|
|
244
|
+
businessModel: asString(value.businessModel) || void 0,
|
|
245
|
+
modelFamily: asString(value.modelFamily) || void 0,
|
|
246
|
+
modelClass: asString(value.modelClass) || void 0,
|
|
247
|
+
regionPolicy: asString(value.regionPolicy) || void 0,
|
|
248
|
+
unit: asString(value.unit) || void 0,
|
|
249
|
+
scarcityLevel: asString(value.scarcityLevel) || void 0,
|
|
250
|
+
hardLimits: objectField(value, "hardLimits", path, diagnostics),
|
|
251
|
+
routingPolicy: objectField(value, "routingPolicy", path, diagnostics),
|
|
252
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function parseProviderRegistration(value, path, diagnostics) {
|
|
256
|
+
if (value === void 0) return void 0;
|
|
257
|
+
if (!isRecord(value)) {
|
|
258
|
+
diagnostics.push(errorDiagnostic("seed.invalid_object", "Expected registration to be an object.", path));
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
const apiKeyValue = value.apiKey;
|
|
262
|
+
if (apiKeyValue === void 0) return {};
|
|
263
|
+
if (!isRecord(apiKeyValue)) {
|
|
264
|
+
diagnostics.push(errorDiagnostic("seed.invalid_object", "Expected registration.apiKey to be an object.", `${path}.apiKey`));
|
|
265
|
+
return {};
|
|
266
|
+
}
|
|
267
|
+
if (apiKeyValue.createIfMissing !== void 0 && typeof apiKeyValue.createIfMissing !== "boolean") {
|
|
268
|
+
diagnostics.push(errorDiagnostic("seed.invalid_boolean", "Expected registration.apiKey.createIfMissing to be a boolean.", `${path}.apiKey.createIfMissing`));
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
apiKey: {
|
|
272
|
+
createIfMissing: typeof apiKeyValue.createIfMissing === "boolean" ? apiKeyValue.createIfMissing : void 0,
|
|
273
|
+
name: asString(apiKeyValue.name) || void 0,
|
|
274
|
+
scopes: stringArrayField(apiKeyValue, "scopes", `${path}.apiKey`, diagnostics),
|
|
275
|
+
expiresAt: asString(apiKeyValue.expiresAt) || void 0
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function parseProvider(value, path, diagnostics) {
|
|
280
|
+
if (!isRecord(value)) {
|
|
281
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected capacity provider resource to be an object.", path));
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const lanesValue = value.lanes;
|
|
285
|
+
const lanes = lanesValue === void 0 ? [] : Array.isArray(lanesValue) ? lanesValue.map((lane, index) => parseLane(lane, `${path}.lanes[${index}]`, diagnostics)).filter((lane) => Boolean(lane)) : (diagnostics.push(errorDiagnostic("seed.invalid_lanes", "Expected lanes to be an array.", `${path}.lanes`)), []);
|
|
286
|
+
return {
|
|
287
|
+
...keyBase(value, path, diagnostics),
|
|
288
|
+
team: requireString(value, "team", path, diagnostics),
|
|
289
|
+
name: requireString(value, "name", path, diagnostics),
|
|
290
|
+
kind: asString(value.kind) || void 0,
|
|
291
|
+
provider: requireString(value, "provider", path, diagnostics),
|
|
292
|
+
billingScope: asString(value.billingScope) || void 0,
|
|
293
|
+
monthlyCreditBudget: numberField(value, "monthlyCreditBudget", path, diagnostics),
|
|
294
|
+
dailyCreditBudget: numberField(value, "dailyCreditBudget", path, diagnostics),
|
|
295
|
+
maxConcurrentWorkdays: numberField(value, "maxConcurrentWorkdays", path, diagnostics),
|
|
296
|
+
maxConcurrentWorkers: numberField(value, "maxConcurrentWorkers", path, diagnostics),
|
|
297
|
+
capacityModel: objectField(value, "capacityModel", path, diagnostics),
|
|
298
|
+
registration: parseProviderRegistration(value.registration, `${path}.registration`, diagnostics),
|
|
299
|
+
metadata: objectField(value, "metadata", path, diagnostics),
|
|
300
|
+
lanes
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function parseGrant(value, path, diagnostics) {
|
|
304
|
+
if (!isRecord(value)) {
|
|
305
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected capacity grant resource to be an object.", path));
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const environment = asString(value.environment);
|
|
309
|
+
if (environment && !ALLOWED_ENVIRONMENTS.has(environment)) {
|
|
310
|
+
diagnostics.push(errorDiagnostic("seed.unknown_environment", `Unknown grant environment: ${environment}.`, `${path}.environment`));
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...keyBase(value, path, diagnostics),
|
|
314
|
+
provider: requireString(value, "provider", path, diagnostics),
|
|
315
|
+
lane: asString(value.lane) || void 0,
|
|
316
|
+
team: requireString(value, "team", path, diagnostics),
|
|
317
|
+
project: asString(value.project) || void 0,
|
|
318
|
+
environment: environment && ALLOWED_ENVIRONMENTS.has(environment) ? environment : void 0,
|
|
319
|
+
grantScope: asString(value.grantScope) || void 0,
|
|
320
|
+
dailyCreditLimit: numberField(value, "dailyCreditLimit", path, diagnostics),
|
|
321
|
+
weeklyCreditLimit: numberField(value, "weeklyCreditLimit", path, diagnostics),
|
|
322
|
+
monthlyCreditLimit: numberField(value, "monthlyCreditLimit", path, diagnostics),
|
|
323
|
+
dailyUsdLimit: numberField(value, "dailyUsdLimit", path, diagnostics),
|
|
324
|
+
weeklyQuotaMinutes: numberField(value, "weeklyQuotaMinutes", path, diagnostics),
|
|
325
|
+
monthlyProviderUnits: numberField(value, "monthlyProviderUnits", path, diagnostics),
|
|
326
|
+
priorityWeight: numberField(value, "priorityWeight", path, diagnostics),
|
|
327
|
+
overflowPolicy: asString(value.overflowPolicy) || void 0,
|
|
328
|
+
state: asString(value.state) || void 0,
|
|
329
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function parseWorkPolicy(value, path, diagnostics) {
|
|
333
|
+
if (!isRecord(value)) {
|
|
334
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource", "Expected work policy resource to be an object.", path));
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const environment = requireString(value, "environment", path, diagnostics);
|
|
338
|
+
if (environment && !ALLOWED_ENVIRONMENTS.has(environment)) {
|
|
339
|
+
diagnostics.push(errorDiagnostic("seed.unknown_environment", `Unknown work policy environment: ${environment}.`, `${path}.environment`));
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
...keyBase(value, path, diagnostics),
|
|
343
|
+
project: requireString(value, "project", path, diagnostics),
|
|
344
|
+
environment: ALLOWED_ENVIRONMENTS.has(environment) ? environment : "local",
|
|
345
|
+
enabled: typeof value.enabled === "boolean" ? value.enabled : void 0,
|
|
346
|
+
startCron: asString(value.startCron) || void 0,
|
|
347
|
+
durationMinutes: numberField(value, "durationMinutes", path, diagnostics),
|
|
348
|
+
maxRunners: numberField(value, "maxRunners", path, diagnostics),
|
|
349
|
+
maxWorkersPerRunner: numberField(value, "maxWorkersPerRunner", path, diagnostics),
|
|
350
|
+
dailyCreditBudget: numberField(value, "dailyCreditBudget", path, diagnostics),
|
|
351
|
+
maxQueuedTasks: numberField(value, "maxQueuedTasks", path, diagnostics),
|
|
352
|
+
maxQueuedCredits: numberField(value, "maxQueuedCredits", path, diagnostics),
|
|
353
|
+
autoscale: objectField(value, "autoscale", path, diagnostics),
|
|
354
|
+
creditWeights: Array.isArray(value.creditWeights) ? value.creditWeights : void 0,
|
|
355
|
+
metadata: objectField(value, "metadata", path, diagnostics)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function validateRepository(repository, path, diagnostics) {
|
|
359
|
+
if (!repository.gitUrl) return;
|
|
360
|
+
if (/^(?:\.{1,2}\/?|packages\/|\/)/u.test(repository.gitUrl) || !/(?:^[a-z][a-z0-9+.-]*:\/\/|^git@)/iu.test(repository.gitUrl)) {
|
|
361
|
+
diagnostics.push(errorDiagnostic("seed.invalid_git_url", "Project repository gitUrl must be a remote Git URL, not a local path.", `${path}.gitUrl`));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (repository.provider !== "github") return;
|
|
365
|
+
const parsed = parseGitHubRepository(repository.gitUrl);
|
|
366
|
+
if (!parsed) {
|
|
367
|
+
diagnostics.push(errorDiagnostic("seed.invalid_git_url", "GitHub repository gitUrl must identify an owner and repository.", `${path}.gitUrl`));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (parsed.owner !== repository.owner || parsed.name !== repository.name) {
|
|
371
|
+
diagnostics.push(errorDiagnostic(
|
|
372
|
+
"seed.repository_metadata_mismatch",
|
|
373
|
+
`gitUrl points to ${parsed.owner}/${parsed.name}, but owner/name are ${repository.owner}/${repository.name}.`,
|
|
374
|
+
path
|
|
375
|
+
));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function parseGitHubRepository(gitUrl) {
|
|
379
|
+
const ssh = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/u.exec(gitUrl);
|
|
380
|
+
if (ssh) return { owner: ssh[1], name: ssh[2] };
|
|
381
|
+
try {
|
|
382
|
+
const url = new URL(gitUrl);
|
|
383
|
+
if (url.hostname !== "github.com") return null;
|
|
384
|
+
const [owner, rawName] = url.pathname.replace(/^\/+/u, "").split("/");
|
|
385
|
+
if (!owner || !rawName) return null;
|
|
386
|
+
return { owner, name: rawName.replace(/\.git$/u, "") };
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function walkForSecrets(value, path, diagnostics) {
|
|
392
|
+
if (typeof value === "string") {
|
|
393
|
+
if (/(?:ghp_|github_pat_|sk-[A-Za-z0-9]|xox[baprs]-|-----BEGIN [A-Z ]*PRIVATE KEY-----)/u.test(value)) {
|
|
394
|
+
diagnostics.push(errorDiagnostic("seed.secret_value", "Manifest appears to contain a raw secret value.", path));
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(value)) {
|
|
399
|
+
value.forEach((entry, index) => walkForSecrets(entry, `${path}[${index}]`, diagnostics));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (!isRecord(value)) return;
|
|
403
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
404
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
405
|
+
if (typeof entry === "string" && /(?:api[_-]?key|token|private[_-]?key|password|secret|credential)/iu.test(key)) {
|
|
406
|
+
if (!CREDENTIAL_REF_PATTERN.test(entry)) {
|
|
407
|
+
diagnostics.push(errorDiagnostic("seed.secret_field", `Field ${key} must use a credential reference, not an inline value.`, nextPath));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
walkForSecrets(entry, nextPath, diagnostics);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function arrayBucket(resources, bucket, diagnostics) {
|
|
414
|
+
const value = resources[bucket];
|
|
415
|
+
if (value === void 0) return [];
|
|
416
|
+
if (!Array.isArray(value)) {
|
|
417
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resource_bucket", `resources.${bucket} must be an array.`, `resources.${bucket}`));
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
if (!SUPPORTED_BUCKETS.has(bucket) && value.length > 0) {
|
|
421
|
+
diagnostics.push(errorDiagnostic("seed.unsupported_resource_kind", `resources.${bucket} is recognized but is not backed by seed reconciliation yet.`, `resources.${bucket}`));
|
|
422
|
+
}
|
|
423
|
+
return value;
|
|
424
|
+
}
|
|
425
|
+
function validateResourceKeys(manifest, diagnostics) {
|
|
426
|
+
const seen = /* @__PURE__ */ new Map();
|
|
427
|
+
const visit = (key, path) => {
|
|
428
|
+
if (!key) return;
|
|
429
|
+
const existingPath = seen.get(key);
|
|
430
|
+
if (existingPath) {
|
|
431
|
+
diagnostics.push(errorDiagnostic("seed.duplicate_key", `Duplicate resource key ${key}; first seen at ${existingPath}.`, path));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
seen.set(key, path);
|
|
435
|
+
};
|
|
436
|
+
manifest.resources.teams.forEach((team, index) => visit(team.key, `resources.teams[${index}].key`));
|
|
437
|
+
manifest.resources.repositoryHosts.forEach((host, index) => visit(host.key, `resources.repositoryHosts[${index}].key`));
|
|
438
|
+
manifest.resources.projects.forEach((project, index) => visit(project.key, `resources.projects[${index}].key`));
|
|
439
|
+
manifest.resources.hubRepositories.forEach((repository, index) => visit(repository.key, `resources.hubRepositories[${index}].key`));
|
|
440
|
+
manifest.resources.products.forEach((product, index) => visit(product.key, `resources.products[${index}].key`));
|
|
441
|
+
manifest.resources.catalogArtifacts.forEach((artifact, index) => visit(artifact.key, `resources.catalogArtifacts[${index}].key`));
|
|
442
|
+
manifest.resources.capacityProviders.forEach((provider, providerIndex) => {
|
|
443
|
+
visit(provider.key, `resources.capacityProviders[${providerIndex}].key`);
|
|
444
|
+
(provider.lanes ?? []).forEach((lane, laneIndex) => visit(lane.key, `resources.capacityProviders[${providerIndex}].lanes[${laneIndex}].key`));
|
|
445
|
+
});
|
|
446
|
+
manifest.resources.capacityGrants.forEach((grant, index) => visit(grant.key, `resources.capacityGrants[${index}].key`));
|
|
447
|
+
manifest.resources.workPolicies.forEach((policy, index) => visit(policy.key, `resources.workPolicies[${index}].key`));
|
|
448
|
+
}
|
|
449
|
+
function validateReferences(manifest, diagnostics) {
|
|
450
|
+
const teamKeys = new Set(manifest.resources.teams.map((team) => team.key));
|
|
451
|
+
const projectKeys = new Set(manifest.resources.projects.map((project) => project.key));
|
|
452
|
+
const repositoryHostKeys = new Set(manifest.resources.repositoryHosts.map((host) => host.key));
|
|
453
|
+
const productKeys = new Set(manifest.resources.products.map((product) => product.key));
|
|
454
|
+
const providerKeys = new Set(manifest.resources.capacityProviders.map((provider) => provider.key));
|
|
455
|
+
const laneKeys = new Set(manifest.resources.capacityProviders.flatMap((provider) => (provider.lanes ?? []).map((lane) => lane.key)));
|
|
456
|
+
manifest.resources.repositoryHosts.forEach((host, index) => {
|
|
457
|
+
if (!teamKeys.has(host.team)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown team reference: ${host.team}.`, `resources.repositoryHosts[${index}].team`));
|
|
458
|
+
});
|
|
459
|
+
manifest.resources.projects.forEach((project, index) => {
|
|
460
|
+
if (!teamKeys.has(project.team)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown team reference: ${project.team}.`, `resources.projects[${index}].team`));
|
|
461
|
+
});
|
|
462
|
+
manifest.resources.hubRepositories.forEach((repository, index) => {
|
|
463
|
+
if (!projectKeys.has(repository.project)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown project reference: ${repository.project}.`, `resources.hubRepositories[${index}].project`));
|
|
464
|
+
if (repository.repositoryHost && !repositoryHostKeys.has(repository.repositoryHost)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown repository host reference: ${repository.repositoryHost}.`, `resources.hubRepositories[${index}].repositoryHost`));
|
|
465
|
+
});
|
|
466
|
+
manifest.resources.products.forEach((product, index) => {
|
|
467
|
+
if (!teamKeys.has(product.team)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown team reference: ${product.team}.`, `resources.products[${index}].team`));
|
|
468
|
+
});
|
|
469
|
+
manifest.resources.catalogArtifacts.forEach((artifact, index) => {
|
|
470
|
+
if (!productKeys.has(artifact.product)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown product reference: ${artifact.product}.`, `resources.catalogArtifacts[${index}].product`));
|
|
471
|
+
});
|
|
472
|
+
manifest.resources.capacityProviders.forEach((provider, index) => {
|
|
473
|
+
if (!teamKeys.has(provider.team)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown team reference: ${provider.team}.`, `resources.capacityProviders[${index}].team`));
|
|
474
|
+
});
|
|
475
|
+
manifest.resources.capacityGrants.forEach((grant, index) => {
|
|
476
|
+
if (!teamKeys.has(grant.team)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown team reference: ${grant.team}.`, `resources.capacityGrants[${index}].team`));
|
|
477
|
+
if (!providerKeys.has(grant.provider)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown capacity provider reference: ${grant.provider}.`, `resources.capacityGrants[${index}].provider`));
|
|
478
|
+
if (grant.lane && !laneKeys.has(grant.lane)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown capacity lane reference: ${grant.lane}.`, `resources.capacityGrants[${index}].lane`));
|
|
479
|
+
if (grant.project && !projectKeys.has(grant.project)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown project reference: ${grant.project}.`, `resources.capacityGrants[${index}].project`));
|
|
480
|
+
});
|
|
481
|
+
manifest.resources.workPolicies.forEach((policy, index) => {
|
|
482
|
+
if (!projectKeys.has(policy.project)) diagnostics.push(errorDiagnostic("seed.invalid_reference", `Unknown project reference: ${policy.project}.`, `resources.workPolicies[${index}].project`));
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function parseSeedManifest(value, diagnostics) {
|
|
486
|
+
walkForSecrets(value, "", diagnostics);
|
|
487
|
+
if (!isRecord(value)) {
|
|
488
|
+
diagnostics.push(errorDiagnostic("seed.invalid_manifest", "Seed manifest must be an object.", "manifest"));
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const name = requireString(value, "name", "manifest", diagnostics);
|
|
492
|
+
if (value.version !== 1) {
|
|
493
|
+
diagnostics.push(errorDiagnostic("seed.unsupported_version", `Unsupported seed manifest version: ${String(value.version)}.`, "version"));
|
|
494
|
+
}
|
|
495
|
+
const environments = parseEnvironments(value.environments, "environments", diagnostics) ?? [];
|
|
496
|
+
if (environments.length === 0) {
|
|
497
|
+
diagnostics.push(errorDiagnostic("seed.missing_environments", "Seed manifest must declare at least one environment.", "environments"));
|
|
498
|
+
}
|
|
499
|
+
const defaultEnvironments = parseEnvironments(value.defaultEnvironments, "defaultEnvironments", diagnostics);
|
|
500
|
+
for (const environment of defaultEnvironments ?? []) {
|
|
501
|
+
if (!environments.includes(environment)) {
|
|
502
|
+
diagnostics.push(errorDiagnostic("seed.default_environment_not_declared", `Default environment ${environment} is not declared in environments.`, "defaultEnvironments"));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const resourcesValue = value.resources;
|
|
506
|
+
if (!isRecord(resourcesValue)) {
|
|
507
|
+
diagnostics.push(errorDiagnostic("seed.invalid_resources", "Seed manifest resources must be an object.", "resources"));
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
for (const bucket of Object.keys(resourcesValue)) {
|
|
511
|
+
if (!RESOURCE_BUCKETS.includes(bucket)) {
|
|
512
|
+
diagnostics.push(errorDiagnostic("seed.unsupported_resource_kind", `Unsupported resource bucket: ${bucket}.`, `resources.${bucket}`));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const resources = {
|
|
516
|
+
teams: arrayBucket(resourcesValue, "teams", diagnostics).map((entry, index) => parseTeam(entry, `resources.teams[${index}]`, diagnostics)).filter((team) => Boolean(team)),
|
|
517
|
+
repositoryHosts: arrayBucket(resourcesValue, "repositoryHosts", diagnostics).map((entry, index) => parseRepositoryHost(entry, `resources.repositoryHosts[${index}]`, diagnostics)).filter((host) => Boolean(host)),
|
|
518
|
+
projects: arrayBucket(resourcesValue, "projects", diagnostics).map((entry, index) => parseProject(entry, `resources.projects[${index}]`, diagnostics)).filter((project) => Boolean(project)),
|
|
519
|
+
hubRepositories: arrayBucket(resourcesValue, "hubRepositories", diagnostics).map((entry, index) => parseHubRepository(entry, `resources.hubRepositories[${index}]`, diagnostics)).filter((repository) => Boolean(repository)),
|
|
520
|
+
products: arrayBucket(resourcesValue, "products", diagnostics).map((entry, index) => parseProduct(entry, `resources.products[${index}]`, diagnostics)).filter((product) => Boolean(product)),
|
|
521
|
+
catalogArtifacts: arrayBucket(resourcesValue, "catalogArtifacts", diagnostics).map((entry, index) => parseCatalogArtifact(entry, `resources.catalogArtifacts[${index}]`, diagnostics)).filter((artifact) => Boolean(artifact)),
|
|
522
|
+
capacityProviders: arrayBucket(resourcesValue, "capacityProviders", diagnostics).map((entry, index) => parseProvider(entry, `resources.capacityProviders[${index}]`, diagnostics)).filter((provider) => Boolean(provider)),
|
|
523
|
+
capacityGrants: arrayBucket(resourcesValue, "capacityGrants", diagnostics).map((entry, index) => parseGrant(entry, `resources.capacityGrants[${index}]`, diagnostics)).filter((grant) => Boolean(grant)),
|
|
524
|
+
workPolicies: arrayBucket(resourcesValue, "workPolicies", diagnostics).map((entry, index) => parseWorkPolicy(entry, `resources.workPolicies[${index}]`, diagnostics)).filter((policy) => Boolean(policy)),
|
|
525
|
+
agentPools: arrayBucket(resourcesValue, "agentPools", diagnostics)
|
|
526
|
+
};
|
|
527
|
+
const manifest = {
|
|
528
|
+
name,
|
|
529
|
+
version: 1,
|
|
530
|
+
description: asString(value.description) || void 0,
|
|
531
|
+
defaultEnvironments,
|
|
532
|
+
environments,
|
|
533
|
+
resources
|
|
534
|
+
};
|
|
535
|
+
validateResourceKeys(manifest, diagnostics);
|
|
536
|
+
validateReferences(manifest, diagnostics);
|
|
537
|
+
if (diagnostics.length === 0 && manifest.resources.projects.length === 0) {
|
|
538
|
+
diagnostics.push(warningDiagnostic("seed.empty_projects", "Seed manifest does not define projects.", "resources.projects"));
|
|
539
|
+
}
|
|
540
|
+
return manifest;
|
|
541
|
+
}
|
|
542
|
+
export {
|
|
543
|
+
parseSeedManifest
|
|
544
|
+
};
|