@agentxm/registry-client 0.28.4-bootstrap.0
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/LICENSE +110 -0
- package/README.md +9 -0
- package/dist/src/__generated__/registry-client.d.ts +6501 -0
- package/dist/src/__generated__/registry-client.js +2270 -0
- package/dist/src/admin-client.d.ts +181 -0
- package/dist/src/admin-client.js +165 -0
- package/dist/src/archive-cache.d.ts +46 -0
- package/dist/src/archive-cache.js +179 -0
- package/dist/src/atomic-write.d.ts +49 -0
- package/dist/src/atomic-write.js +60 -0
- package/dist/src/axm-package-meta.d.ts +31 -0
- package/dist/src/axm-package-meta.js +31 -0
- package/dist/src/cache-root.d.ts +11 -0
- package/dist/src/cache-root.js +43 -0
- package/dist/src/client.d.ts +270 -0
- package/dist/src/client.js +43 -0
- package/dist/src/deprecation-warning.d.ts +3 -0
- package/dist/src/deprecation-warning.js +16 -0
- package/dist/src/error-mapping.d.ts +82 -0
- package/dist/src/error-mapping.js +186 -0
- package/dist/src/errors.d.ts +107 -0
- package/dist/src/errors.js +98 -0
- package/dist/src/failure-mapping.d.ts +14 -0
- package/dist/src/failure-mapping.js +100 -0
- package/dist/src/fs-helpers.d.ts +12 -0
- package/dist/src/fs-helpers.js +13 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +37 -0
- package/dist/src/integrity.d.ts +13 -0
- package/dist/src/integrity.js +17 -0
- package/dist/src/local-client.d.ts +24 -0
- package/dist/src/local-client.js +815 -0
- package/dist/src/network.d.ts +6 -0
- package/dist/src/network.js +6 -0
- package/dist/src/path-safety.d.ts +18 -0
- package/dist/src/path-safety.js +26 -0
- package/dist/src/purl-match.d.ts +28 -0
- package/dist/src/purl-match.js +35 -0
- package/dist/src/registry-url.d.ts +14 -0
- package/dist/src/registry-url.js +12 -0
- package/dist/src/remote-client.d.ts +27 -0
- package/dist/src/remote-client.js +725 -0
- package/dist/src/request-policy.d.ts +29 -0
- package/dist/src/request-policy.js +190 -0
- package/dist/src/response-body.d.ts +11 -0
- package/dist/src/response-body.js +32 -0
- package/dist/src/retry-after.d.ts +9 -0
- package/dist/src/retry-after.js +28 -0
- package/dist/src/translate.d.ts +20 -0
- package/dist/src/translate.js +170 -0
- package/dist/src/utils.d.ts +60 -0
- package/dist/src/utils.js +187 -0
- package/package.json +55 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local filesystem-backed registry client.
|
|
3
|
+
*
|
|
4
|
+
* All operations read/write files relative to a registry root using the
|
|
5
|
+
* layout: `<root>/extensions/@<owner>/<type>/<name>/`.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import * as Array from "effect/Array";
|
|
12
|
+
import * as DateTime from "effect/DateTime";
|
|
13
|
+
import * as Duration from "effect/Duration";
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import * as Option from "effect/Option";
|
|
16
|
+
import * as Schema from "effect/Schema";
|
|
17
|
+
import * as Semaphore from "effect/Semaphore";
|
|
18
|
+
import { RegistryOperationFailed } from "./errors.js";
|
|
19
|
+
import { ExtensionFqnSchema, toAuthor, } from "@agentxm/extension-model/unstable/extensions";
|
|
20
|
+
import { isExtensionTypePlural, parseExtensionFqnParts, toExtensionTypePlural, } from "@agentxm/extension-model/unstable/extensions/common";
|
|
21
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
22
|
+
import { packagesToPackageUrlParts, ExtensionIndexSchema, } from "@agentxm/registry-protocol/unstable/registry/schema";
|
|
23
|
+
import { purlMatch } from "./purl-match.js";
|
|
24
|
+
import { PackageUrlSchema, } from "@agentxm/extension-model/unstable/packaging/package-url";
|
|
25
|
+
import { extensionDir, extensionLifecycleWarnings, pluralizeType, resolveVersionEntry, selectVersion, } from "./utils.js";
|
|
26
|
+
import { PUBLICATION_SET_CONTRACT, evaluateProspectivePackDependencyState, publicationDescriptorDigest, publicationSetDigest, validatePublicationDescriptors, validatePublicationSetResponse, } from "@agentxm/registry-protocol/unstable/registry/publication-set";
|
|
27
|
+
/** A publish target version already exists: a conflict naming the version. */
|
|
28
|
+
const publishConflict = (args) => new RegistryOperationFailed({
|
|
29
|
+
category: "conflict",
|
|
30
|
+
detail: args.version === undefined
|
|
31
|
+
? "Version already exists."
|
|
32
|
+
: `Version ${args.version} already exists.`,
|
|
33
|
+
suggestions: [{ description: "Bump the version in your manifest." }],
|
|
34
|
+
...(args.cause === undefined ? {} : { cause: args.cause }),
|
|
35
|
+
});
|
|
36
|
+
/** The registry rejected a publish: a validation failure carrying guidance. */
|
|
37
|
+
const registryPublishRejected = (args) => new RegistryOperationFailed({
|
|
38
|
+
category: "validation",
|
|
39
|
+
detail: args.message,
|
|
40
|
+
suggestions: args.suggestions ?? [
|
|
41
|
+
{ description: "Check the extension package and try again." },
|
|
42
|
+
],
|
|
43
|
+
...(args.cause === undefined ? {} : { cause: args.cause }),
|
|
44
|
+
});
|
|
45
|
+
const decodeExtensionIndexFromJsonString = Schema.decodeUnknownEffect(Schema.fromJsonString(ExtensionIndexSchema));
|
|
46
|
+
const encodeExtensionIndexToJsonString = Schema.encodeSync(Schema.fromJsonString(ExtensionIndexSchema));
|
|
47
|
+
const encodePackageUrl = Schema.encodeSync(PackageUrlSchema);
|
|
48
|
+
const PUBLISH_LOCK_RETRY_DELAY = Duration.millis(25);
|
|
49
|
+
const PUBLISH_LOCK_STALE_TIMEOUT = Duration.minutes(5);
|
|
50
|
+
// eslint-disable-next-line no-restricted-syntax -- Process-owned keys are bounded by packages published during this one CLI invocation.
|
|
51
|
+
const publishLockSemaphores = new Map();
|
|
52
|
+
const localVisibilityRevision = (index) => `local-${createHash("sha256")
|
|
53
|
+
.update(JSON.stringify({
|
|
54
|
+
owner: index.owner,
|
|
55
|
+
type: index.type,
|
|
56
|
+
name: index.name,
|
|
57
|
+
visibility: index.visibility ?? "public",
|
|
58
|
+
}))
|
|
59
|
+
.digest("hex")}`;
|
|
60
|
+
const evaluateLocalPublishVisibility = (args) => {
|
|
61
|
+
const target = Schema.decodeUnknownSync(ExtensionFqnSchema)(`${args.target.owner}/${toExtensionTypePlural(args.target.type)}/${args.target.name}`);
|
|
62
|
+
const conflict = args.input.intent !== null &&
|
|
63
|
+
args.input.request !== null &&
|
|
64
|
+
args.input.intent.value !== args.input.request
|
|
65
|
+
? [
|
|
66
|
+
{
|
|
67
|
+
code: "visibility/intent-conflict",
|
|
68
|
+
severity: "error",
|
|
69
|
+
message: `Requested visibility '${args.input.request}' conflicts with repository intent '${args.input.intent.value}'.`,
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
: [];
|
|
73
|
+
if (args.index === undefined) {
|
|
74
|
+
const resolved = args.input.intent !== null
|
|
75
|
+
? {
|
|
76
|
+
value: args.input.intent.value,
|
|
77
|
+
disposition: "establish",
|
|
78
|
+
source: args.input.intent.source,
|
|
79
|
+
}
|
|
80
|
+
: args.input.request !== null
|
|
81
|
+
? { value: args.input.request, disposition: "establish", source: "explicit" }
|
|
82
|
+
: { value: "public", disposition: "establish", source: "platform" };
|
|
83
|
+
return {
|
|
84
|
+
target,
|
|
85
|
+
intent: args.input.intent,
|
|
86
|
+
request: args.input.request,
|
|
87
|
+
resolved,
|
|
88
|
+
actual: null,
|
|
89
|
+
comparison: "not-established",
|
|
90
|
+
findings: conflict,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const actual = args.index.visibility ?? "public";
|
|
94
|
+
const drift = args.input.intent !== null && args.input.intent.value !== actual
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
code: "visibility/drift",
|
|
98
|
+
severity: "error",
|
|
99
|
+
message: `Repository intent '${args.input.intent.value}' does not match Registry visibility '${actual}'.`,
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
: [];
|
|
103
|
+
return {
|
|
104
|
+
target,
|
|
105
|
+
intent: args.input.intent,
|
|
106
|
+
request: args.input.request,
|
|
107
|
+
resolved: { value: actual, disposition: "preserve", source: "existing" },
|
|
108
|
+
actual: { value: actual, revision: localVisibilityRevision(args.index) },
|
|
109
|
+
comparison: args.input.intent === null
|
|
110
|
+
? "unconfigured"
|
|
111
|
+
: args.input.intent.value === actual
|
|
112
|
+
? "match"
|
|
113
|
+
: "drift",
|
|
114
|
+
findings: [...conflict, ...drift],
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const resolveLocalUploadVisibility = (index, visibility) => index === undefined
|
|
118
|
+
? (visibility ?? { value: "public", disposition: "establish", source: "platform" })
|
|
119
|
+
: { value: index.visibility ?? "public", disposition: "preserve", source: "existing" };
|
|
120
|
+
const makeLocalPublishCondition = (args) => `"pv1-${createHash("sha256")
|
|
121
|
+
.update(JSON.stringify({
|
|
122
|
+
target: args.target,
|
|
123
|
+
visibility: args.visibility,
|
|
124
|
+
targetVersionExists: args.targetVersionExists,
|
|
125
|
+
}))
|
|
126
|
+
.digest("hex")}"`;
|
|
127
|
+
// -----------------------------------------------------------------------------
|
|
128
|
+
// Helpers
|
|
129
|
+
// -----------------------------------------------------------------------------
|
|
130
|
+
const readExtensionIndex = (fs, idxPath) => Effect.gen(function* () {
|
|
131
|
+
const content = yield* fs.readFileString(idxPath).pipe(Effect.mapError((e) => new RegistryOperationFailed({
|
|
132
|
+
category: "internal",
|
|
133
|
+
detail: `Failed to read index: ${idxPath}`,
|
|
134
|
+
cause: e,
|
|
135
|
+
})));
|
|
136
|
+
return yield* decodeExtensionIndexFromJsonString(content).pipe(Effect.mapError((e) => new RegistryOperationFailed({
|
|
137
|
+
category: "internal",
|
|
138
|
+
detail: `Invalid index schema: ${idxPath}`,
|
|
139
|
+
cause: e,
|
|
140
|
+
})));
|
|
141
|
+
});
|
|
142
|
+
const removeBestEffort = (fs, filePath) => fs.remove(filePath).pipe(Effect.ignore);
|
|
143
|
+
const inProcessPublishSemaphoreFor = (lockPath) => {
|
|
144
|
+
const existing = publishLockSemaphores.get(lockPath);
|
|
145
|
+
if (existing !== undefined)
|
|
146
|
+
return existing;
|
|
147
|
+
const created = Semaphore.makeUnsafe(1);
|
|
148
|
+
publishLockSemaphores.set(lockPath, created);
|
|
149
|
+
return created;
|
|
150
|
+
};
|
|
151
|
+
const acquirePublishLock = (fs, lockPath) => Effect.gen(function* () {
|
|
152
|
+
const acquiredAt = DateTime.formatIso(yield* DateTime.now);
|
|
153
|
+
const result = yield* fs
|
|
154
|
+
.writeFileString(lockPath, `${acquiredAt}\n`, { flag: "wx" })
|
|
155
|
+
.pipe(Effect.result);
|
|
156
|
+
if (result._tag === "Success")
|
|
157
|
+
return;
|
|
158
|
+
if (result.failure.reason._tag !== "AlreadyExists") {
|
|
159
|
+
return yield* new RegistryOperationFailed({
|
|
160
|
+
category: "internal",
|
|
161
|
+
detail: `Failed to acquire local registry publish lock: ${lockPath}`,
|
|
162
|
+
cause: result.failure,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const info = yield* fs.stat(lockPath).pipe(Effect.option);
|
|
166
|
+
const staleLock = Option.isSome(info) && Option.isSome(info.value.mtime)
|
|
167
|
+
? yield* DateTime.isPast(DateTime.addDuration(DateTime.makeUnsafe(info.value.mtime.value), PUBLISH_LOCK_STALE_TIMEOUT))
|
|
168
|
+
: false;
|
|
169
|
+
if (staleLock) {
|
|
170
|
+
yield* removeBestEffort(fs, lockPath);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
yield* Effect.sleep(PUBLISH_LOCK_RETRY_DELAY);
|
|
174
|
+
}
|
|
175
|
+
return yield* acquirePublishLock(fs, lockPath);
|
|
176
|
+
});
|
|
177
|
+
const withPublishLock = (fs, lockPath, effect) => inProcessPublishSemaphoreFor(lockPath).withPermits(1)(Effect.scoped(Effect.gen(function* () {
|
|
178
|
+
yield* Effect.acquireRelease(acquirePublishLock(fs, lockPath), () => removeBestEffort(fs, lockPath));
|
|
179
|
+
return yield* effect;
|
|
180
|
+
})));
|
|
181
|
+
const indexToManifest = (index, versionRange) => {
|
|
182
|
+
const selectedVersion = resolveVersionEntry(index.versions, versionRange);
|
|
183
|
+
if (Option.isNone(selectedVersion))
|
|
184
|
+
return Option.none();
|
|
185
|
+
const ver = selectedVersion.value;
|
|
186
|
+
const lifecycleWarnings = extensionLifecycleWarnings(index, ver);
|
|
187
|
+
return Option.some({
|
|
188
|
+
owner: index.owner,
|
|
189
|
+
type: index.type,
|
|
190
|
+
name: index.name,
|
|
191
|
+
publisherBindingId: index.publisherBindingId,
|
|
192
|
+
description: Option.fromUndefinedOr(index.description),
|
|
193
|
+
repository: Option.fromUndefinedOr(index.repository),
|
|
194
|
+
bugs: Option.fromUndefinedOr(index.bugs),
|
|
195
|
+
license: Option.fromUndefinedOr(index.license),
|
|
196
|
+
authors: Option.match(Option.fromUndefinedOr(index.authors), {
|
|
197
|
+
onNone: () => [],
|
|
198
|
+
onSome: (authors) => authors.map((author) => toAuthor(author)),
|
|
199
|
+
}),
|
|
200
|
+
dependencies: ver.dependencies ?? {},
|
|
201
|
+
version: ver.version,
|
|
202
|
+
integrity: ver.integrity,
|
|
203
|
+
packages: packagesToPackageUrlParts(ver.packages),
|
|
204
|
+
...(index.deprecation === null ? {} : { deprecation: index.deprecation }),
|
|
205
|
+
...(lifecycleWarnings.length === 0 ? {} : { lifecycleWarnings }),
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Process a single name directory within a registry owner/type directory.
|
|
210
|
+
* Reads the index.json, validates it, and selects a matching version.
|
|
211
|
+
* Returns Some(RegistryExtensionManifest) if a matching version is found, None otherwise.
|
|
212
|
+
*/
|
|
213
|
+
const processNameDir = (fs, path, typeDir, nameDir, versionRange) => Effect.gen(function* () {
|
|
214
|
+
const dir = path.join(typeDir, nameDir);
|
|
215
|
+
const idxPath = path.join(dir, "index.json");
|
|
216
|
+
const idxExists = yield* fs.exists(idxPath).pipe(Effect.orElseSucceed(() => false));
|
|
217
|
+
if (!idxExists)
|
|
218
|
+
return Option.none();
|
|
219
|
+
const index = yield* readExtensionIndex(fs, idxPath);
|
|
220
|
+
return indexToManifest(index, versionRange);
|
|
221
|
+
});
|
|
222
|
+
const packageIdentity = (parts) => ({
|
|
223
|
+
type: parts.type,
|
|
224
|
+
name: parts.name,
|
|
225
|
+
...(parts.namespace === undefined ? {} : { namespace: parts.namespace }),
|
|
226
|
+
...(parts.qualifiers === undefined ? {} : { qualifiers: parts.qualifiers }),
|
|
227
|
+
...(parts.subpath === undefined ? {} : { subpath: parts.subpath }),
|
|
228
|
+
});
|
|
229
|
+
const extensionDeclarationToDiscoveryRef = (value) => {
|
|
230
|
+
const parts = parseExtensionFqnParts(value.ref);
|
|
231
|
+
if (parts === undefined) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
ref: `${parts.owner}/${toExtensionTypePlural(parts.type)}/${parts.name}`,
|
|
236
|
+
...(value.versionRange === undefined || value.versionRange === null
|
|
237
|
+
? {}
|
|
238
|
+
: { versionRange: value.versionRange }),
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
const indexToExtensionResult = (index, attestedBy, official) => {
|
|
242
|
+
const [latestVersion] = index.versions;
|
|
243
|
+
if (latestVersion === undefined) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
ref: `${index.owner}/${toExtensionTypePlural(index.type)}/${index.name}`,
|
|
248
|
+
resolved: true,
|
|
249
|
+
extension: {
|
|
250
|
+
type: index.type,
|
|
251
|
+
name: index.name,
|
|
252
|
+
owner: index.owner,
|
|
253
|
+
installVersion: latestVersion.version,
|
|
254
|
+
},
|
|
255
|
+
attestedBy,
|
|
256
|
+
official,
|
|
257
|
+
packageVersionInRange: true,
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
/** Parse an extension FQN string into owner/type/name parts. */
|
|
261
|
+
const parseRef = (ref) => parseExtensionFqnParts(ref);
|
|
262
|
+
/**
|
|
263
|
+
* Scan all extensions under the extensions root directory.
|
|
264
|
+
* Returns an array of ExtensionIndex entries.
|
|
265
|
+
*/
|
|
266
|
+
const scanAllExtensions = (fs, path, extensionsRoot) => Effect.gen(function* () {
|
|
267
|
+
const ownerDirs = yield* fs
|
|
268
|
+
.readDirectory(extensionsRoot)
|
|
269
|
+
.pipe(Effect.orElseSucceed(() => []));
|
|
270
|
+
// Cap concurrency at each nesting level to bound resource usage on large registries.
|
|
271
|
+
const nestedResults = yield* Effect.forEach(ownerDirs.filter((d) => d.startsWith("@")), (ownerDir) => Effect.gen(function* () {
|
|
272
|
+
const ownerPath = path.join(extensionsRoot, ownerDir);
|
|
273
|
+
const typeDirs = yield* fs
|
|
274
|
+
.readDirectory(ownerPath)
|
|
275
|
+
.pipe(Effect.orElseSucceed(() => []));
|
|
276
|
+
const typeResults = yield* Effect.forEach(typeDirs.filter((d) => isExtensionTypePlural(d)), (typeDir) => Effect.gen(function* () {
|
|
277
|
+
const typePath = path.join(ownerPath, typeDir);
|
|
278
|
+
const nameDirs = yield* fs
|
|
279
|
+
.readDirectory(typePath)
|
|
280
|
+
.pipe(Effect.orElseSucceed(() => []));
|
|
281
|
+
return yield* Effect.forEach(nameDirs, (nameDir) => Effect.gen(function* () {
|
|
282
|
+
const idxPath = path.join(typePath, nameDir, "index.json");
|
|
283
|
+
const exists = yield* fs
|
|
284
|
+
.exists(idxPath)
|
|
285
|
+
.pipe(Effect.orElseSucceed(() => false));
|
|
286
|
+
if (!exists)
|
|
287
|
+
return Option.none();
|
|
288
|
+
const index = yield* readExtensionIndex(fs, idxPath);
|
|
289
|
+
if (index.versions.length === 0)
|
|
290
|
+
return Option.none();
|
|
291
|
+
return Option.some(index);
|
|
292
|
+
}), { concurrency: 20 }).pipe(Effect.map(Array.getSomes));
|
|
293
|
+
}), { concurrency: 20 });
|
|
294
|
+
return Array.flatten(typeResults);
|
|
295
|
+
}), { concurrency: 20 });
|
|
296
|
+
return Array.flatten(nestedResults);
|
|
297
|
+
});
|
|
298
|
+
// -----------------------------------------------------------------------------
|
|
299
|
+
// Local Registry Client
|
|
300
|
+
// -----------------------------------------------------------------------------
|
|
301
|
+
/**
|
|
302
|
+
* Creates a local filesystem-backed registry client.
|
|
303
|
+
*
|
|
304
|
+
* All operations read/write files relative to `registryRoot` using the
|
|
305
|
+
* registry layout: `<root>/extensions/@<owner>/<type>/<name>/`.
|
|
306
|
+
*
|
|
307
|
+
* @param registryRoot - Absolute path to the registry root directory
|
|
308
|
+
*
|
|
309
|
+
* @experimental This API is unstable and may change without notice.
|
|
310
|
+
*/
|
|
311
|
+
export const createLocalRegistryClient = (registryRoot, fs, path) => ({
|
|
312
|
+
previewExtensionPublishes: (args) => Effect.gen(function* () {
|
|
313
|
+
const descriptors = yield* Effect.try({
|
|
314
|
+
try: () => validatePublicationDescriptors(args.candidates),
|
|
315
|
+
catch: (cause) => new RegistryOperationFailed({
|
|
316
|
+
category: "validation",
|
|
317
|
+
detail: "The publication set is invalid.",
|
|
318
|
+
cause,
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
const candidates = yield* Effect.forEach(descriptors, (descriptor) => {
|
|
322
|
+
const target = descriptor.target;
|
|
323
|
+
const indexPath = path.join(extensionDir(registryRoot, target.owner, target.type, target.name, path.join), "index.json");
|
|
324
|
+
return Effect.gen(function* () {
|
|
325
|
+
const exists = yield* fs.exists(indexPath).pipe(Effect.orElseSucceed(() => false));
|
|
326
|
+
const index = exists ? yield* readExtensionIndex(fs, indexPath) : undefined;
|
|
327
|
+
const visibility = evaluateLocalPublishVisibility({
|
|
328
|
+
target,
|
|
329
|
+
index,
|
|
330
|
+
input: descriptor.visibility,
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
kind: "resolved",
|
|
334
|
+
target,
|
|
335
|
+
participation: descriptor.participation,
|
|
336
|
+
descriptorDigest: publicationDescriptorDigest(descriptor),
|
|
337
|
+
visibility,
|
|
338
|
+
...(descriptor.participation === "verified-existing"
|
|
339
|
+
? {}
|
|
340
|
+
: {
|
|
341
|
+
condition: makeLocalPublishCondition({
|
|
342
|
+
target,
|
|
343
|
+
visibility: visibility.resolved,
|
|
344
|
+
targetVersionExists: index?.versions.some((entry) => entry.version === target.version) ?? false,
|
|
345
|
+
}),
|
|
346
|
+
}),
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
const candidateByTarget = new Map(candidates.map((candidate) => [
|
|
351
|
+
`${candidate.target.owner}\u0000${candidate.target.type}\u0000${candidate.target.name}`,
|
|
352
|
+
candidate,
|
|
353
|
+
]));
|
|
354
|
+
const packDescriptors = descriptors.filter((descriptor) => descriptor.target.type === "pack" && descriptor.participation === "publish");
|
|
355
|
+
const dependencyDescriptors = packDescriptors.flatMap((descriptor) => descriptor.pack?.dependencies ?? []);
|
|
356
|
+
const snapshots = yield* Effect.forEach(dependencyDescriptors, (dependency) => Effect.gen(function* () {
|
|
357
|
+
const indexPath = path.join(extensionDir(registryRoot, dependency.owner, dependency.type, dependency.name, path.join), "index.json");
|
|
358
|
+
const exists = yield* fs.exists(indexPath).pipe(Effect.orElseSucceed(() => false));
|
|
359
|
+
const index = exists ? yield* readExtensionIndex(fs, indexPath) : undefined;
|
|
360
|
+
return {
|
|
361
|
+
dependency,
|
|
362
|
+
exists: index !== undefined,
|
|
363
|
+
visibility: index?.visibility ?? (index === undefined ? null : "public"),
|
|
364
|
+
lifecycleState: index === undefined ? null : "active",
|
|
365
|
+
deprecation: index?.deprecation ?? null,
|
|
366
|
+
versions: index?.versions.map((version) => ({
|
|
367
|
+
version: version.version,
|
|
368
|
+
status: "available",
|
|
369
|
+
yanked: false,
|
|
370
|
+
purged: false,
|
|
371
|
+
})) ?? [],
|
|
372
|
+
};
|
|
373
|
+
}));
|
|
374
|
+
const prospectiveCandidates = descriptors.map((descriptor) => {
|
|
375
|
+
const result = candidateByTarget.get(`${descriptor.target.owner}\u0000${descriptor.target.type}\u0000${descriptor.target.name}`);
|
|
376
|
+
if (result?.kind === "resolved") {
|
|
377
|
+
return { descriptor, kind: "resolved", visibility: result.visibility };
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
descriptor,
|
|
381
|
+
kind: "unavailable",
|
|
382
|
+
visibility: {
|
|
383
|
+
target: Schema.decodeUnknownSync(ExtensionFqnSchema)(`${descriptor.target.owner}/${toExtensionTypePlural(descriptor.target.type)}/${descriptor.target.name}`),
|
|
384
|
+
unavailable: true,
|
|
385
|
+
findings: [
|
|
386
|
+
{
|
|
387
|
+
code: "visibility/unavailable",
|
|
388
|
+
severity: "error",
|
|
389
|
+
message: "The visibility target is unavailable.",
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
const packs = packDescriptors.map((descriptor) => {
|
|
396
|
+
const packCandidate = candidateByTarget.get(`${descriptor.target.owner}\u0000${descriptor.target.type}\u0000${descriptor.target.name}`);
|
|
397
|
+
const evaluated = evaluateProspectivePackDependencyState({
|
|
398
|
+
packVisibility: packCandidate?.kind === "resolved" && packCandidate.visibility.resolved !== null
|
|
399
|
+
? packCandidate.visibility.resolved.value
|
|
400
|
+
: "public",
|
|
401
|
+
dependencies: descriptor.pack?.dependencies ?? [],
|
|
402
|
+
snapshots,
|
|
403
|
+
candidates: prospectiveCandidates,
|
|
404
|
+
});
|
|
405
|
+
const blocked = evaluated.findings.some((finding) => finding.severity === "error");
|
|
406
|
+
return {
|
|
407
|
+
target: descriptor.target,
|
|
408
|
+
status: blocked ? "blocked" : "admitted",
|
|
409
|
+
findings: evaluated.findings,
|
|
410
|
+
resolutions: blocked ? [] : evaluated.resolutions,
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
const status = candidates.some((candidate) => candidate.visibility.findings.some((finding) => finding.severity === "error")) || packs.some((pack) => pack.status === "blocked")
|
|
414
|
+
? "blocked"
|
|
415
|
+
: "admitted";
|
|
416
|
+
const boundCandidates = candidates.map((candidate) => status === "blocked" && candidate.kind === "resolved"
|
|
417
|
+
? {
|
|
418
|
+
kind: candidate.kind,
|
|
419
|
+
target: candidate.target,
|
|
420
|
+
participation: candidate.participation,
|
|
421
|
+
descriptorDigest: candidate.descriptorDigest,
|
|
422
|
+
visibility: candidate.visibility,
|
|
423
|
+
}
|
|
424
|
+
: candidate);
|
|
425
|
+
return validatePublicationSetResponse(descriptors, {
|
|
426
|
+
contract: PUBLICATION_SET_CONTRACT,
|
|
427
|
+
publicationSetDigest: publicationSetDigest(descriptors),
|
|
428
|
+
status,
|
|
429
|
+
candidates: boundCandidates,
|
|
430
|
+
packs,
|
|
431
|
+
});
|
|
432
|
+
}),
|
|
433
|
+
getExtensionsByScope: (args) => Effect.gen(function* () {
|
|
434
|
+
if (args.owner === "*") {
|
|
435
|
+
const extensionsDir = path.join(registryRoot, "extensions");
|
|
436
|
+
const indexes = yield* scanAllExtensions(fs, path, extensionsDir);
|
|
437
|
+
const manifests = Array.getSomes(indexes
|
|
438
|
+
.filter((index) => args.types.length === 0 || args.types.includes(index.type))
|
|
439
|
+
.filter((index) => args.names.length === 0 || args.names.includes(index.name))
|
|
440
|
+
.map((index) => indexToManifest(index, Option.none())));
|
|
441
|
+
const total = manifests.length;
|
|
442
|
+
const sliced = manifests.slice(args.offset);
|
|
443
|
+
const extensions = Option.match(args.limit, {
|
|
444
|
+
onNone: () => sliced,
|
|
445
|
+
onSome: (l) => sliced.slice(0, l),
|
|
446
|
+
});
|
|
447
|
+
return {
|
|
448
|
+
extensions,
|
|
449
|
+
total,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const findForName = (name) => Effect.gen(function* () {
|
|
453
|
+
const requestedTypes = args.types.length === 0 ? ["skill", "mcp-server", "pack"] : args.types;
|
|
454
|
+
const extensionsDir = path.join(registryRoot, "extensions");
|
|
455
|
+
const nestedResults = yield* Effect.forEach(requestedTypes, (extType) => Effect.gen(function* () {
|
|
456
|
+
const typeDir = path.join(extensionsDir, args.owner, pluralizeType(extType));
|
|
457
|
+
const typeDirExists = yield* fs
|
|
458
|
+
.exists(typeDir)
|
|
459
|
+
.pipe(Effect.orElseSucceed(() => false));
|
|
460
|
+
if (!typeDirExists)
|
|
461
|
+
return [];
|
|
462
|
+
const nameDirs = yield* fs
|
|
463
|
+
.readDirectory(typeDir)
|
|
464
|
+
.pipe(Effect.orElseSucceed(() => []));
|
|
465
|
+
const filtered = name !== "" ? nameDirs.filter((d) => d === name) : nameDirs;
|
|
466
|
+
return yield* Effect.forEach(filtered, (nameDir) => processNameDir(fs, path, typeDir, nameDir, Option.none()), { concurrency: "unbounded" }).pipe(Effect.map(Array.getSomes));
|
|
467
|
+
}), { concurrency: "unbounded" });
|
|
468
|
+
return Array.flatten(nestedResults);
|
|
469
|
+
});
|
|
470
|
+
const all = args.names.length > 0
|
|
471
|
+
? yield* Effect.forEach(args.names, (name) => findForName(name), {
|
|
472
|
+
concurrency: "unbounded",
|
|
473
|
+
}).pipe(Effect.map(Array.flatten))
|
|
474
|
+
: yield* findForName("");
|
|
475
|
+
const total = all.length;
|
|
476
|
+
const offset = args.offset;
|
|
477
|
+
const sliced = all.slice(offset);
|
|
478
|
+
const extensions = Option.match(args.limit, {
|
|
479
|
+
onNone: () => sliced,
|
|
480
|
+
onSome: (l) => sliced.slice(0, l),
|
|
481
|
+
});
|
|
482
|
+
return {
|
|
483
|
+
extensions,
|
|
484
|
+
total,
|
|
485
|
+
};
|
|
486
|
+
}),
|
|
487
|
+
ownerExists: (owner) => Effect.gen(function* () {
|
|
488
|
+
const scopeDir = path.join(registryRoot, "extensions", owner);
|
|
489
|
+
const exists = yield* fs.exists(scopeDir).pipe(Effect.orElseSucceed(() => false));
|
|
490
|
+
return { exists };
|
|
491
|
+
}),
|
|
492
|
+
getExtensionIndex: (args) => Effect.gen(function* () {
|
|
493
|
+
const dir = extensionDir(registryRoot, args.owner, args.type, args.name, path.join);
|
|
494
|
+
const idxPath = path.join(dir, "index.json");
|
|
495
|
+
const exists = yield* fs.exists(idxPath).pipe(Effect.orElseSucceed(() => false));
|
|
496
|
+
if (!exists) {
|
|
497
|
+
return Option.none();
|
|
498
|
+
}
|
|
499
|
+
return Option.some(yield* readExtensionIndex(fs, idxPath));
|
|
500
|
+
}),
|
|
501
|
+
getExactExtensionVersion: (args) => Effect.gen(function* () {
|
|
502
|
+
const dir = extensionDir(registryRoot, args.owner, args.type, args.name, path.join);
|
|
503
|
+
const idxPath = path.join(dir, "index.json");
|
|
504
|
+
const exists = yield* fs.exists(idxPath).pipe(Effect.orElseSucceed(() => false));
|
|
505
|
+
if (!exists)
|
|
506
|
+
return Option.none();
|
|
507
|
+
const index = yield* readExtensionIndex(fs, idxPath);
|
|
508
|
+
const version = index.versions.find((entry) => entry.version === args.version);
|
|
509
|
+
return version === undefined
|
|
510
|
+
? Option.none()
|
|
511
|
+
: Option.some({
|
|
512
|
+
owner: args.owner,
|
|
513
|
+
type: args.type,
|
|
514
|
+
name: args.name,
|
|
515
|
+
version: args.version,
|
|
516
|
+
integrity: version.integrity,
|
|
517
|
+
status: "available",
|
|
518
|
+
});
|
|
519
|
+
}),
|
|
520
|
+
getExtensionPackage: (args) => Effect.gen(function* () {
|
|
521
|
+
const owner = args.owner;
|
|
522
|
+
const dir = extensionDir(registryRoot, owner, args.type, args.name, path.join);
|
|
523
|
+
const version = yield* Option.match(args.version, {
|
|
524
|
+
onNone: () => Effect.gen(function* () {
|
|
525
|
+
const idxPath = path.join(dir, "index.json");
|
|
526
|
+
const index = yield* readExtensionIndex(fs, idxPath);
|
|
527
|
+
const selected = selectVersion(index.versions);
|
|
528
|
+
if (Option.isNone(selected)) {
|
|
529
|
+
return yield* new RegistryOperationFailed({
|
|
530
|
+
category: "internal",
|
|
531
|
+
detail: `No versions found for ${owner}/${args.type}/${args.name}`,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return selected.value.version;
|
|
535
|
+
}),
|
|
536
|
+
onSome: (requestedVersion) => Effect.gen(function* () {
|
|
537
|
+
const requestedArchivePath = path.join(dir, `${requestedVersion}.zip`);
|
|
538
|
+
const requestedExists = yield* fs
|
|
539
|
+
.exists(requestedArchivePath)
|
|
540
|
+
.pipe(Effect.orElseSucceed(() => false));
|
|
541
|
+
// Fast path: exact version archive exists.
|
|
542
|
+
if (requestedExists) {
|
|
543
|
+
return requestedVersion;
|
|
544
|
+
}
|
|
545
|
+
// Fallback: treat requested version as semver constraint (e.g. ^1.0.0).
|
|
546
|
+
const idxPath = path.join(dir, "index.json");
|
|
547
|
+
const index = yield* readExtensionIndex(fs, idxPath);
|
|
548
|
+
const selected = resolveVersionEntry(index.versions, Option.some(requestedVersion));
|
|
549
|
+
if (Option.isNone(selected)) {
|
|
550
|
+
return yield* new RegistryOperationFailed({
|
|
551
|
+
category: "internal",
|
|
552
|
+
detail: `No version matched constraint "${requestedVersion}" for ${owner}/${args.type}/${args.name}`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return selected.value.version;
|
|
556
|
+
}),
|
|
557
|
+
});
|
|
558
|
+
const archivePath = path.join(dir, `${version}.zip`);
|
|
559
|
+
const exists = yield* fs.exists(archivePath).pipe(Effect.orElseSucceed(() => false));
|
|
560
|
+
if (!exists) {
|
|
561
|
+
return yield* new RegistryOperationFailed({
|
|
562
|
+
category: "internal",
|
|
563
|
+
detail: `Archive not found: ${archivePath}`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const archive = yield* fs.readFile(archivePath).pipe(Effect.mapError((e) => new RegistryOperationFailed({
|
|
567
|
+
category: "internal",
|
|
568
|
+
detail: `Failed to read archive: ${archivePath}`,
|
|
569
|
+
cause: e,
|
|
570
|
+
})));
|
|
571
|
+
return { archive };
|
|
572
|
+
}),
|
|
573
|
+
publishExtension: (args) => Effect.gen(function* () {
|
|
574
|
+
const owner = args.owner;
|
|
575
|
+
const dir = extensionDir(registryRoot, owner, args.type, args.name, path.join);
|
|
576
|
+
// Ensure directory exists
|
|
577
|
+
yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.mapError((e) => registryPublishRejected({
|
|
578
|
+
message: `Registry directory could not be created: ${dir}`,
|
|
579
|
+
cause: e,
|
|
580
|
+
})));
|
|
581
|
+
const indexPath = path.join(dir, "index.json");
|
|
582
|
+
const archivePath = path.join(dir, `${args.version}.zip`);
|
|
583
|
+
const lockPath = path.join(dir, ".publish.lock");
|
|
584
|
+
return yield* withPublishLock(fs, lockPath, Effect.gen(function* () {
|
|
585
|
+
const indexExists = yield* fs.exists(indexPath).pipe(Effect.orElseSucceed(() => false));
|
|
586
|
+
const currentIndex = indexExists
|
|
587
|
+
? yield* fs.readFileString(indexPath).pipe(Effect.flatMap(decodeExtensionIndexFromJsonString), Effect.mapError((cause) => new RegistryOperationFailed({
|
|
588
|
+
category: "internal",
|
|
589
|
+
detail: "Registry index schema is invalid",
|
|
590
|
+
cause,
|
|
591
|
+
})))
|
|
592
|
+
: undefined;
|
|
593
|
+
const resolvedVisibility = resolveLocalUploadVisibility(currentIndex, args.visibility);
|
|
594
|
+
if (args.condition !== undefined) {
|
|
595
|
+
const currentCondition = makeLocalPublishCondition({
|
|
596
|
+
target: {
|
|
597
|
+
owner: args.owner,
|
|
598
|
+
type: args.type,
|
|
599
|
+
name: args.name,
|
|
600
|
+
version: args.version,
|
|
601
|
+
},
|
|
602
|
+
visibility: resolvedVisibility,
|
|
603
|
+
targetVersionExists: currentIndex?.versions.some((entry) => entry.version === args.version) ?? false,
|
|
604
|
+
});
|
|
605
|
+
if (args.condition !== currentCondition) {
|
|
606
|
+
return yield* new RegistryOperationFailed({
|
|
607
|
+
category: "conflict",
|
|
608
|
+
detail: "Publish precondition changed; preview again before publishing.",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const nextIndex = indexExists
|
|
613
|
+
? yield* Effect.gen(function* () {
|
|
614
|
+
if (currentIndex === undefined) {
|
|
615
|
+
return yield* new RegistryOperationFailed({
|
|
616
|
+
category: "internal",
|
|
617
|
+
detail: "Registry index disappeared during publication.",
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (args.condition === undefined && args.visibility !== undefined) {
|
|
621
|
+
return yield* new RegistryOperationFailed({
|
|
622
|
+
category: "conflict",
|
|
623
|
+
detail: "Initial visibility is only valid when creating an extension.",
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (currentIndex.versions.some((version) => version.version === args.version)) {
|
|
627
|
+
return yield* publishConflict({ version: args.version });
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
...currentIndex,
|
|
631
|
+
versions: [args.metadata, ...currentIndex.versions],
|
|
632
|
+
};
|
|
633
|
+
})
|
|
634
|
+
: {
|
|
635
|
+
name: args.name,
|
|
636
|
+
owner,
|
|
637
|
+
type: args.type,
|
|
638
|
+
publisherBindingId: `hbnd_local_${globalThis.crypto.randomUUID()}`,
|
|
639
|
+
visibility: resolvedVisibility.value,
|
|
640
|
+
deprecation: null,
|
|
641
|
+
versions: [args.metadata],
|
|
642
|
+
};
|
|
643
|
+
yield* writeFileAtomic(fs, {
|
|
644
|
+
targetPath: archivePath,
|
|
645
|
+
content: args.archive,
|
|
646
|
+
removeTargetBeforeRename: true,
|
|
647
|
+
mapError: (failure) => failure.step === "rename"
|
|
648
|
+
? registryPublishRejected({
|
|
649
|
+
message: `Registry archive could not be committed: ${archivePath}`,
|
|
650
|
+
cause: failure.cause,
|
|
651
|
+
})
|
|
652
|
+
: registryPublishRejected({
|
|
653
|
+
message: `Registry archive temp file could not be written: ${failure.tempPath}`,
|
|
654
|
+
cause: failure.cause,
|
|
655
|
+
}),
|
|
656
|
+
});
|
|
657
|
+
yield* writeFileAtomic(fs, {
|
|
658
|
+
targetPath: indexPath,
|
|
659
|
+
content: `${encodeExtensionIndexToJsonString(nextIndex)}\n`,
|
|
660
|
+
mapError: (failure) => failure.step === "rename"
|
|
661
|
+
? registryPublishRejected({
|
|
662
|
+
message: `Registry index could not be committed: ${indexPath}`,
|
|
663
|
+
cause: failure.cause,
|
|
664
|
+
})
|
|
665
|
+
: registryPublishRejected({
|
|
666
|
+
message: `Registry index temp file could not be written: ${failure.tempPath}`,
|
|
667
|
+
cause: failure.cause,
|
|
668
|
+
}),
|
|
669
|
+
});
|
|
670
|
+
return {
|
|
671
|
+
published: true,
|
|
672
|
+
owner: args.owner,
|
|
673
|
+
type: args.type,
|
|
674
|
+
name: args.name,
|
|
675
|
+
version: args.version,
|
|
676
|
+
integrity: args.metadata.integrity,
|
|
677
|
+
status: "available",
|
|
678
|
+
visibility: resolvedVisibility,
|
|
679
|
+
warnings: [],
|
|
680
|
+
};
|
|
681
|
+
}));
|
|
682
|
+
}),
|
|
683
|
+
getExtensionVisibility: (args) => Effect.gen(function* () {
|
|
684
|
+
const dir = extensionDir(registryRoot, args.owner, args.type, args.name, path.join);
|
|
685
|
+
const index = yield* readExtensionIndex(fs, path.join(dir, "index.json"));
|
|
686
|
+
return evaluateLocalPublishVisibility({
|
|
687
|
+
target: { owner: args.owner, type: args.type, name: args.name },
|
|
688
|
+
index,
|
|
689
|
+
input: { intent: args.intent, request: null },
|
|
690
|
+
});
|
|
691
|
+
}),
|
|
692
|
+
updateExtensionVisibility: (args) => Effect.gen(function* () {
|
|
693
|
+
const target = parseExtensionFqnParts(args.target);
|
|
694
|
+
if (target === undefined) {
|
|
695
|
+
return yield* new RegistryOperationFailed({
|
|
696
|
+
category: "validation",
|
|
697
|
+
detail: `Invalid extension target: ${args.target}`,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const dir = extensionDir(registryRoot, target.owner, target.type, target.name, path.join);
|
|
701
|
+
const indexPath = path.join(dir, "index.json");
|
|
702
|
+
const content = yield* fs.readFileString(indexPath).pipe(Effect.mapError((cause) => new RegistryOperationFailed({
|
|
703
|
+
category: "not_found",
|
|
704
|
+
detail: `Extension index not found: ${indexPath}`,
|
|
705
|
+
cause,
|
|
706
|
+
})));
|
|
707
|
+
const index = yield* decodeExtensionIndexFromJsonString(content).pipe(Effect.mapError((cause) => new RegistryOperationFailed({
|
|
708
|
+
category: "validation",
|
|
709
|
+
detail: "Registry index schema is invalid",
|
|
710
|
+
cause,
|
|
711
|
+
})));
|
|
712
|
+
const revision = localVisibilityRevision(index);
|
|
713
|
+
if (revision !== args.revision) {
|
|
714
|
+
return yield* new RegistryOperationFailed({
|
|
715
|
+
category: "conflict",
|
|
716
|
+
detail: "Extension visibility changed; read the current revision and retry.",
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const before = index.visibility ?? "public";
|
|
720
|
+
if (before === args.visibility) {
|
|
721
|
+
return {
|
|
722
|
+
target: args.target,
|
|
723
|
+
before,
|
|
724
|
+
after: before,
|
|
725
|
+
authority: args.authority,
|
|
726
|
+
result: "already-satisfied",
|
|
727
|
+
revision,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
yield* fs
|
|
731
|
+
.writeFileString(indexPath, `${encodeExtensionIndexToJsonString({ ...index, visibility: args.visibility })}\n`)
|
|
732
|
+
.pipe(Effect.mapError((cause) => new RegistryOperationFailed({
|
|
733
|
+
category: "internal",
|
|
734
|
+
detail: `Registry index could not be written: ${indexPath}`,
|
|
735
|
+
cause,
|
|
736
|
+
})));
|
|
737
|
+
const updated = { ...index, visibility: args.visibility };
|
|
738
|
+
return {
|
|
739
|
+
target: args.target,
|
|
740
|
+
before,
|
|
741
|
+
after: args.visibility,
|
|
742
|
+
authority: args.authority,
|
|
743
|
+
result: "changed",
|
|
744
|
+
revision: localVisibilityRevision(updated),
|
|
745
|
+
};
|
|
746
|
+
}),
|
|
747
|
+
extensionExists: (args) => Effect.gen(function* () {
|
|
748
|
+
const owner = args.owner;
|
|
749
|
+
const dir = extensionDir(registryRoot, owner, args.type, args.name, path.join);
|
|
750
|
+
const indexPath = path.join(dir, "index.json");
|
|
751
|
+
const exists = yield* fs.exists(indexPath).pipe(Effect.orElseSucceed(() => false));
|
|
752
|
+
return { exists };
|
|
753
|
+
}),
|
|
754
|
+
discoverPackages: (args) => Effect.gen(function* () {
|
|
755
|
+
const extensionsRoot = path.join(registryRoot, "extensions");
|
|
756
|
+
const rootExists = yield* fs.exists(extensionsRoot).pipe(Effect.orElseSucceed(() => false));
|
|
757
|
+
if (!rootExists) {
|
|
758
|
+
return { results: [] };
|
|
759
|
+
}
|
|
760
|
+
// Scan all extensions and read their index.json
|
|
761
|
+
const allExtensions = yield* scanAllExtensions(fs, path, extensionsRoot);
|
|
762
|
+
const results = args.packages.map((pkg) => {
|
|
763
|
+
const purl = encodePackageUrl(packageIdentity(pkg.purl));
|
|
764
|
+
const entries = new Map();
|
|
765
|
+
for (const spec of pkg.declaredExtensions) {
|
|
766
|
+
const declared = extensionDeclarationToDiscoveryRef(spec);
|
|
767
|
+
if (declared === undefined) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const parsed = parseRef(spec.ref);
|
|
771
|
+
const match = parsed === undefined
|
|
772
|
+
? undefined
|
|
773
|
+
: allExtensions.find((ext) => ext.owner === parsed.owner &&
|
|
774
|
+
ext.type === parsed.type &&
|
|
775
|
+
ext.name === parsed.name);
|
|
776
|
+
const resolved = match === undefined
|
|
777
|
+
? {
|
|
778
|
+
ref: declared.ref,
|
|
779
|
+
resolved: false,
|
|
780
|
+
attestedBy: ["package"],
|
|
781
|
+
official: false,
|
|
782
|
+
packageVersionInRange: true,
|
|
783
|
+
}
|
|
784
|
+
: indexToExtensionResult(match, ["package"], false);
|
|
785
|
+
if (resolved !== undefined) {
|
|
786
|
+
entries.set(declared.ref, resolved);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
for (const ext of allExtensions) {
|
|
790
|
+
const latestVersion = ext.versions[0];
|
|
791
|
+
if (latestVersion === undefined) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const matchesPackage = packagesToPackageUrlParts(latestVersion.packages).some((declared) => purlMatch(pkg.purl, declared));
|
|
795
|
+
if (!matchesPackage) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const ref = `${ext.owner}/${toExtensionTypePlural(ext.type)}/${ext.name}`;
|
|
799
|
+
const existing = entries.get(ref);
|
|
800
|
+
const next = indexToExtensionResult(ext, existing === undefined ? ["extension"] : ["package", "extension"], existing !== undefined);
|
|
801
|
+
if (next !== undefined) {
|
|
802
|
+
entries.set(ref, next);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
purl,
|
|
807
|
+
version: pkg.version,
|
|
808
|
+
status: "resolved",
|
|
809
|
+
extensions: [...entries.values()],
|
|
810
|
+
};
|
|
811
|
+
});
|
|
812
|
+
return { results };
|
|
813
|
+
}),
|
|
814
|
+
});
|
|
815
|
+
//# sourceMappingURL=local-client.js.map
|