@epicdm/flowstate-extension-core 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1042 @@
1
+ 'use strict';
2
+
3
+ var fflate = require('fflate');
4
+
5
+ // src/manifest-adapter.ts
6
+ var CATEGORY_MAP = {
7
+ "Azure": "azure",
8
+ "Data Science": "data-science",
9
+ "Debuggers": "debuggers",
10
+ "Education": "education",
11
+ "Extension Packs": "extension-packs",
12
+ "Formatters": "formatters",
13
+ "Keymaps": "keymaps",
14
+ "Language Packs": "language-packs",
15
+ "Linters": "linters",
16
+ "Machine Learning": "machine-learning",
17
+ "Notebooks": "notebooks",
18
+ "Other": "other",
19
+ "Programming Languages": "programming-languages",
20
+ "SCM Providers": "scm-providers",
21
+ "Snippets": "snippets",
22
+ "Testing": "testing",
23
+ "Themes": "themes",
24
+ "Visualization": "visualization"
25
+ };
26
+ function mapCategory(vsCodeCategory) {
27
+ if (vsCodeCategory === void 0) return void 0;
28
+ return CATEGORY_MAP[vsCodeCategory];
29
+ }
30
+ function mapActivationEvents(manifest) {
31
+ const events = manifest.activationEvents;
32
+ if (!events || events.length === 0) return [];
33
+ return events.map((event) => {
34
+ if (event === "onStartupFinished") return "onStartup";
35
+ return event;
36
+ });
37
+ }
38
+ function mapExtensionKind(manifest) {
39
+ const contributes = manifest.contributes;
40
+ if (contributes) {
41
+ const contributionKeys = Object.keys(contributes).filter(
42
+ (key) => contributes[key] !== void 0
43
+ );
44
+ const themeKeys = /* @__PURE__ */ new Set(["themes", "iconThemes", "productIconThemes", "colors"]);
45
+ const isThemeOnly = contributionKeys.length > 0 && contributionKeys.every((key) => themeKeys.has(key));
46
+ if (isThemeOnly) return "theme";
47
+ if (contributes.localizations && contributes.localizations.length > 0) {
48
+ return "pack";
49
+ }
50
+ }
51
+ if (manifest.extensionKind?.includes("workspace")) {
52
+ return "app";
53
+ }
54
+ return "ui-extension";
55
+ }
56
+ function vsCodeToFlowState(manifest) {
57
+ const displayName = typeof manifest.displayName === "string" ? manifest.displayName : manifest.displayName?.value ?? manifest.name;
58
+ const result = {
59
+ id: `${manifest.publisher}.${manifest.name}`,
60
+ displayName,
61
+ version: manifest.version,
62
+ publisher: manifest.publisher,
63
+ main: manifest.main ?? manifest.browser ?? ""
64
+ };
65
+ if (manifest.browser !== void 0) {
66
+ result.browser = manifest.browser;
67
+ }
68
+ const category = mapCategory(manifest.categories?.[0]);
69
+ if (category !== void 0) {
70
+ result.category = category;
71
+ }
72
+ const extensionType = mapExtensionKind(manifest);
73
+ if (extensionType !== "ui-extension") {
74
+ result.extensionType = extensionType;
75
+ }
76
+ const activationEvents = mapActivationEvents(manifest);
77
+ if (activationEvents.length > 0) {
78
+ result.activationEvents = activationEvents;
79
+ }
80
+ if (manifest.contributes !== void 0) {
81
+ result.contributes = manifest.contributes;
82
+ }
83
+ return result;
84
+ }
85
+ var REVERSE_CATEGORY_MAP = Object.fromEntries(
86
+ Object.entries(CATEGORY_MAP).map(([vsCode, flowstate]) => [flowstate, vsCode])
87
+ );
88
+ function reverseCategory(flowstateCategory) {
89
+ if (flowstateCategory === void 0) return void 0;
90
+ return REVERSE_CATEGORY_MAP[flowstateCategory];
91
+ }
92
+ function reverseActivationEvents(events) {
93
+ if (!events || events.length === 0) return [];
94
+ return events.map((event) => {
95
+ if (event === "onStartup") return "onStartupFinished";
96
+ return event;
97
+ });
98
+ }
99
+ function reverseExtensionKind(extensionType) {
100
+ switch (extensionType) {
101
+ case "app":
102
+ return ["workspace"];
103
+ case "theme":
104
+ case "pack":
105
+ return ["ui"];
106
+ default:
107
+ return void 0;
108
+ }
109
+ }
110
+ function flowStateToVsCode(manifest) {
111
+ const dotIndex = manifest.id.indexOf(".");
112
+ const name = dotIndex >= 0 ? manifest.id.slice(dotIndex + 1) : manifest.id;
113
+ const publisher = manifest.publisher ?? (dotIndex >= 0 ? manifest.id.slice(0, dotIndex) : "");
114
+ const result = {
115
+ name,
116
+ publisher,
117
+ version: manifest.version,
118
+ displayName: manifest.displayName
119
+ };
120
+ if (manifest.main) {
121
+ result.main = manifest.main;
122
+ }
123
+ if (manifest.browser !== void 0) {
124
+ result.browser = manifest.browser;
125
+ }
126
+ const vsCodeCategory = reverseCategory(manifest.category);
127
+ if (vsCodeCategory !== void 0) {
128
+ result.categories = [vsCodeCategory];
129
+ }
130
+ const extensionKind = reverseExtensionKind(manifest.extensionType);
131
+ if (extensionKind !== void 0) {
132
+ result.extensionKind = extensionKind;
133
+ }
134
+ const activationEvents = reverseActivationEvents(manifest.activationEvents);
135
+ if (activationEvents.length > 0) {
136
+ result.activationEvents = activationEvents;
137
+ }
138
+ if (manifest.contributes !== void 0) {
139
+ result.contributes = manifest.contributes;
140
+ }
141
+ return result;
142
+ }
143
+ var DECLARATIVE_CONTRIBUTION_POINTS = /* @__PURE__ */ new Set([
144
+ "themes",
145
+ "iconThemes",
146
+ "productIconThemes",
147
+ "colors",
148
+ "grammars",
149
+ "snippets",
150
+ "languages",
151
+ "keybindings",
152
+ "configuration",
153
+ "configurationDefaults",
154
+ "jsonValidation",
155
+ "localizations",
156
+ "semanticTokenTypes",
157
+ "semanticTokenModifiers",
158
+ "semanticTokenScopes"
159
+ ]);
160
+ var VsixScanError = class _VsixScanError extends Error {
161
+ constructor(code, message) {
162
+ super(message);
163
+ Object.setPrototypeOf(this, _VsixScanError.prototype);
164
+ this.name = "VsixScanError";
165
+ this.code = code;
166
+ }
167
+ };
168
+ function classifyDeclarative(manifest) {
169
+ if (manifest.main || manifest.browser) return false;
170
+ const contributes = manifest.contributes;
171
+ if (!contributes) return true;
172
+ const keys = Object.keys(contributes).filter(
173
+ (key) => contributes[key] !== void 0
174
+ );
175
+ if (keys.length === 0) return true;
176
+ return keys.every((key) => DECLARATIVE_CONTRIBUTION_POINTS.has(key));
177
+ }
178
+ function enumerateContributionPoints(manifest) {
179
+ const contributes = manifest.contributes;
180
+ if (!contributes) return [];
181
+ return Object.keys(contributes).filter((key) => {
182
+ const value = contributes[key];
183
+ if (value === void 0 || value === null) return false;
184
+ if (Array.isArray(value) && value.length === 0) return false;
185
+ if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
186
+ return false;
187
+ return true;
188
+ }).sort();
189
+ }
190
+ var textDecoder = new TextDecoder();
191
+ var VSCODE_NAMESPACE_PATTERN = /\bvscode\.(\w+)/g;
192
+ function detectApiNamespaces(assets) {
193
+ const namespaces = /* @__PURE__ */ new Set();
194
+ for (const [path, content] of assets) {
195
+ if (!/\.[cm]?js$/.test(path)) continue;
196
+ const text = textDecoder.decode(content);
197
+ let match;
198
+ VSCODE_NAMESPACE_PATTERN.lastIndex = 0;
199
+ while ((match = VSCODE_NAMESPACE_PATTERN.exec(text)) !== null) {
200
+ namespaces.add(`vscode.${match[1]}`);
201
+ }
202
+ }
203
+ return Array.from(namespaces).sort();
204
+ }
205
+ function buildCompatibilityReport(manifest, apiNamespaces) {
206
+ const contributionKeys = enumerateContributionPoints(manifest);
207
+ const supported = contributionKeys.filter((key) => DECLARATIVE_CONTRIBUTION_POINTS.has(key));
208
+ const unsupported = contributionKeys.filter((key) => !DECLARATIVE_CONTRIBUTION_POINTS.has(key));
209
+ const warnings = [];
210
+ if (manifest.main || manifest.browser) {
211
+ warnings.push("Extension has a code entry point and requires an extension host to run.");
212
+ }
213
+ if (unsupported.length > 0) {
214
+ warnings.push(
215
+ `Unsupported contribution points: ${unsupported.join(", ")}. These require a VS Code extension host.`
216
+ );
217
+ }
218
+ if (apiNamespaces.length > 0) {
219
+ warnings.push(`Uses VS Code API namespaces: ${apiNamespaces.join(", ")}.`);
220
+ }
221
+ const hasEntryPoint = Boolean(manifest.main || manifest.browser);
222
+ const needsRuntime = hasEntryPoint || apiNamespaces.length > 0 || unsupported.length > 0;
223
+ let level;
224
+ if (!needsRuntime && supported.length > 0) {
225
+ level = "declarative-only";
226
+ } else if (needsRuntime && supported.length > 0) {
227
+ level = "partial";
228
+ } else {
229
+ level = "unsupported";
230
+ }
231
+ return {
232
+ level,
233
+ supported,
234
+ unsupported,
235
+ apiNamespacesUsed: apiNamespaces,
236
+ warnings
237
+ };
238
+ }
239
+ var EXTENSION_PREFIX = "extension/";
240
+ function scanVsix(data) {
241
+ let entries;
242
+ try {
243
+ entries = fflate.unzipSync(data);
244
+ } catch {
245
+ throw new VsixScanError("INVALID_ZIP", "Failed to decompress VSIX archive: invalid ZIP data.");
246
+ }
247
+ const assets = /* @__PURE__ */ new Map();
248
+ for (const [path, content] of Object.entries(entries)) {
249
+ if (path.startsWith(EXTENSION_PREFIX)) {
250
+ const relativePath = path.slice(EXTENSION_PREFIX.length);
251
+ if (relativePath.length > 0) {
252
+ assets.set(relativePath, content);
253
+ }
254
+ }
255
+ }
256
+ const manifestData = assets.get("package.json");
257
+ if (!manifestData) {
258
+ throw new VsixScanError(
259
+ "MISSING_MANIFEST",
260
+ "VSIX archive does not contain extension/package.json."
261
+ );
262
+ }
263
+ let rawManifest;
264
+ try {
265
+ rawManifest = JSON.parse(textDecoder.decode(manifestData));
266
+ } catch {
267
+ throw new VsixScanError(
268
+ "INVALID_MANIFEST",
269
+ "Failed to parse extension/package.json: invalid JSON."
270
+ );
271
+ }
272
+ const engines = rawManifest["engines"];
273
+ if (typeof rawManifest["name"] !== "string" || typeof rawManifest["publisher"] !== "string" || typeof rawManifest["version"] !== "string" || typeof engines !== "object" || engines === null || typeof engines["vscode"] !== "string") {
274
+ throw new VsixScanError(
275
+ "INVALID_MANIFEST",
276
+ "Extension manifest missing required fields: name, publisher, version, and engines.vscode are required."
277
+ );
278
+ }
279
+ const vsCodeManifest = rawManifest;
280
+ const flowstateManifest = vsCodeToFlowState(vsCodeManifest);
281
+ const isDeclarative = classifyDeclarative(vsCodeManifest);
282
+ const contributionPoints = enumerateContributionPoints(vsCodeManifest);
283
+ const apiNamespaces = detectApiNamespaces(assets);
284
+ const compatibility = buildCompatibilityReport(vsCodeManifest, apiNamespaces);
285
+ return {
286
+ vsCodeManifest,
287
+ flowstateManifest,
288
+ isDeclarative,
289
+ contributionPoints,
290
+ assets,
291
+ compatibility
292
+ };
293
+ }
294
+
295
+ // src/compatibility-scorer.ts
296
+ var SUPPORT_WEIGHTS = {
297
+ native: 1,
298
+ polyfill: 0.8,
299
+ partial: 0.5,
300
+ unsupported: 0
301
+ };
302
+ var PLATFORM_SUPPORT = {
303
+ desktop: {
304
+ themes: "native",
305
+ iconThemes: "native",
306
+ productIconThemes: "native",
307
+ colors: "native",
308
+ grammars: "native",
309
+ snippets: "native",
310
+ languages: "native",
311
+ keybindings: "native",
312
+ configuration: "native",
313
+ configurationDefaults: "native",
314
+ jsonValidation: "native",
315
+ localizations: "native",
316
+ semanticTokenTypes: "native",
317
+ semanticTokenModifiers: "native",
318
+ semanticTokenScopes: "native",
319
+ commands: "polyfill",
320
+ menus: "polyfill",
321
+ views: "partial",
322
+ viewsContainers: "partial"
323
+ },
324
+ web: {
325
+ themes: "native",
326
+ iconThemes: "native",
327
+ productIconThemes: "native",
328
+ colors: "native",
329
+ grammars: "native",
330
+ snippets: "native",
331
+ languages: "native",
332
+ keybindings: "native",
333
+ configuration: "native",
334
+ configurationDefaults: "native",
335
+ jsonValidation: "native",
336
+ localizations: "native",
337
+ semanticTokenTypes: "native",
338
+ semanticTokenModifiers: "native",
339
+ semanticTokenScopes: "native"
340
+ },
341
+ mobile: {
342
+ themes: "native",
343
+ colors: "native",
344
+ snippets: "native",
345
+ languages: "native",
346
+ keybindings: "partial",
347
+ configuration: "native",
348
+ configurationDefaults: "native",
349
+ localizations: "native"
350
+ }
351
+ };
352
+ var PLATFORMS_WITH_EXTENSION_HOST = /* @__PURE__ */ new Set(["desktop"]);
353
+ var ENTRY_POINT_PENALTY = 0.5;
354
+ function getPlatformSupport(platform) {
355
+ return { ...PLATFORM_SUPPORT[platform] };
356
+ }
357
+ function scoreExtension(manifest, platform, apiNamespaces = []) {
358
+ const contributionKeys = enumerateContributionPoints(manifest);
359
+ const platformSupport = PLATFORM_SUPPORT[platform];
360
+ const breakdown = contributionKeys.map((key) => {
361
+ const status = platformSupport[key] ?? "unsupported";
362
+ return {
363
+ key,
364
+ status,
365
+ weight: SUPPORT_WEIGHTS[status]
366
+ };
367
+ });
368
+ let score;
369
+ if (breakdown.length === 0) {
370
+ score = 0;
371
+ } else {
372
+ const totalWeight = breakdown.reduce((sum, point) => sum + point.weight, 0);
373
+ score = Math.round(totalWeight / breakdown.length * 100);
374
+ }
375
+ const hasEntryPoint = Boolean(manifest.main || manifest.browser);
376
+ if (hasEntryPoint && !PLATFORMS_WITH_EXTENSION_HOST.has(platform)) {
377
+ score = Math.round(score * ENTRY_POINT_PENALTY);
378
+ }
379
+ const level = deriveLevel(score);
380
+ const recommendations = generateRecommendations(breakdown, manifest, platform);
381
+ const report = buildCompatibilityReport(manifest, apiNamespaces);
382
+ return {
383
+ score,
384
+ level,
385
+ platform,
386
+ breakdown,
387
+ report,
388
+ recommendations
389
+ };
390
+ }
391
+ function generateRecommendations(breakdown, manifest, platform) {
392
+ const recommendations = [];
393
+ const hasEntryPoint = Boolean(manifest.main || manifest.browser);
394
+ const unsupportedPoints = breakdown.filter((p) => p.status === "unsupported");
395
+ const partialPoints = breakdown.filter((p) => p.status === "partial");
396
+ const allNative = breakdown.length > 0 && breakdown.every((p) => p.status === "native");
397
+ if (hasEntryPoint && !PLATFORMS_WITH_EXTENSION_HOST.has(platform)) {
398
+ recommendations.push(
399
+ "This extension requires an extension host \u2014 only declarative contributions will work on this platform."
400
+ );
401
+ }
402
+ if (unsupportedPoints.length > 0 && platform !== "desktop") {
403
+ const pointNames = unsupportedPoints.map((p) => p.key).join(", ");
404
+ recommendations.push(
405
+ `Consider using the desktop platform for full support of: ${pointNames}.`
406
+ );
407
+ }
408
+ if (partialPoints.length > 0) {
409
+ const pointNames = partialPoints.map((p) => p.key).join(", ");
410
+ recommendations.push(
411
+ `Partial support for: ${pointNames}. Some features may not work as expected.`
412
+ );
413
+ }
414
+ if (allNative && (!hasEntryPoint || PLATFORMS_WITH_EXTENSION_HOST.has(platform))) {
415
+ recommendations.push("All contribution points are natively supported on this platform.");
416
+ }
417
+ return recommendations;
418
+ }
419
+ function deriveLevel(score) {
420
+ if (score >= 90) return "full";
421
+ if (score >= 50) return "partial";
422
+ if (score > 0) return "declarative-only";
423
+ return "unsupported";
424
+ }
425
+
426
+ // src/dependency-resolver.ts
427
+ function normalizeExtensionId(id) {
428
+ return id.toLowerCase();
429
+ }
430
+ function extractDependencyNode(manifest) {
431
+ const id = normalizeExtensionId(`${manifest.publisher}.${manifest.name}`);
432
+ const extensionDependencies = (manifest.extensionDependencies ?? []).map(normalizeExtensionId);
433
+ const extensionPack = (manifest.extensionPack ?? []).map(normalizeExtensionId);
434
+ return { id, extensionDependencies, extensionPack };
435
+ }
436
+ function buildDependencyGraph(nodes) {
437
+ const nodeMap = /* @__PURE__ */ new Map();
438
+ const edges = [];
439
+ const adjacency = /* @__PURE__ */ new Map();
440
+ for (const node of nodes) {
441
+ nodeMap.set(node.id, node);
442
+ if (!adjacency.has(node.id)) {
443
+ adjacency.set(node.id, /* @__PURE__ */ new Set());
444
+ }
445
+ }
446
+ for (const node of nodes) {
447
+ for (const dep of node.extensionDependencies) {
448
+ edges.push({ from: node.id, to: dep, type: "dependency" });
449
+ adjacency.get(node.id).add(dep);
450
+ if (!nodeMap.has(dep)) {
451
+ nodeMap.set(dep, { id: dep, extensionDependencies: [], extensionPack: [] });
452
+ adjacency.set(dep, /* @__PURE__ */ new Set());
453
+ }
454
+ }
455
+ for (const member of node.extensionPack) {
456
+ edges.push({ from: node.id, to: member, type: "pack" });
457
+ adjacency.get(node.id).add(member);
458
+ if (!nodeMap.has(member)) {
459
+ nodeMap.set(member, { id: member, extensionDependencies: [], extensionPack: [] });
460
+ adjacency.set(member, /* @__PURE__ */ new Set());
461
+ }
462
+ }
463
+ }
464
+ return { nodes: nodeMap, edges, adjacency };
465
+ }
466
+ function resolveDependencies(nodes) {
467
+ const graph = buildDependencyGraph(nodes);
468
+ if (graph.nodes.size === 0) {
469
+ return { ok: true, order: [], graph };
470
+ }
471
+ const reverseAdj = /* @__PURE__ */ new Map();
472
+ for (const id of graph.nodes.keys()) {
473
+ reverseAdj.set(id, /* @__PURE__ */ new Set());
474
+ }
475
+ for (const [id, deps] of graph.adjacency) {
476
+ for (const dep of deps) {
477
+ reverseAdj.get(dep).add(id);
478
+ }
479
+ }
480
+ const inDegree = /* @__PURE__ */ new Map();
481
+ for (const [id, deps] of graph.adjacency) {
482
+ inDegree.set(id, deps.size);
483
+ }
484
+ const queue = [];
485
+ for (const [id, degree] of inDegree) {
486
+ if (degree === 0) {
487
+ queue.push(id);
488
+ }
489
+ }
490
+ queue.sort();
491
+ const order = [];
492
+ let queueIdx = 0;
493
+ while (queueIdx < queue.length) {
494
+ const current = queue[queueIdx++];
495
+ order.push(current);
496
+ const freed = [];
497
+ for (const dependent of reverseAdj.get(current)) {
498
+ const newDegree = inDegree.get(dependent) - 1;
499
+ inDegree.set(dependent, newDegree);
500
+ if (newDegree === 0) {
501
+ freed.push(dependent);
502
+ }
503
+ }
504
+ if (freed.length > 0) {
505
+ freed.sort();
506
+ queue.push(...freed);
507
+ }
508
+ }
509
+ if (order.length < graph.nodes.size) {
510
+ const visited = new Set(order);
511
+ const participants = Array.from(graph.nodes.keys()).filter((id) => !visited.has(id)).sort();
512
+ return { ok: false, error: "circular-dependency", participants, graph };
513
+ }
514
+ return { ok: true, order, graph };
515
+ }
516
+
517
+ // src/openvsx-client.ts
518
+ var DEFAULT_BASE_URL = "https://open-vsx.org/api";
519
+ var OpenVSXClient = class {
520
+ constructor(options) {
521
+ const raw = options?.baseUrl ?? DEFAULT_BASE_URL;
522
+ this.baseUrl = raw.endsWith("/") ? raw.slice(0, -1) : raw;
523
+ this.fetch = options?.fetch ?? globalThis.fetch;
524
+ }
525
+ // -------------------------------------------------------------------------
526
+ // Search & query
527
+ // -------------------------------------------------------------------------
528
+ /**
529
+ * Search extensions by text query.
530
+ *
531
+ * @param params - Search parameters (query, category, sort, pagination)
532
+ * @returns Paginated search results
533
+ */
534
+ async search(params) {
535
+ const queryParams = {};
536
+ if (params) {
537
+ if (params.query !== void 0) queryParams["query"] = params.query;
538
+ if (params.category !== void 0) queryParams["category"] = params.category;
539
+ if (params.targetPlatform !== void 0) queryParams["targetPlatform"] = params.targetPlatform;
540
+ if (params.size !== void 0) queryParams["size"] = String(params.size);
541
+ if (params.offset !== void 0) queryParams["offset"] = String(params.offset);
542
+ if (params.sortOrder !== void 0) queryParams["sortOrder"] = params.sortOrder;
543
+ if (params.sortBy !== void 0) queryParams["sortBy"] = params.sortBy;
544
+ if (params.includeAllVersions !== void 0)
545
+ queryParams["includeAllVersions"] = String(params.includeAllVersions);
546
+ }
547
+ return this._get("/-/search", queryParams);
548
+ }
549
+ /**
550
+ * Query extensions with structured field matching.
551
+ *
552
+ * @param params - Query parameters (namespace, extension name, ID, UUID, etc.)
553
+ * @returns Paginated query results with full extension metadata
554
+ */
555
+ async query(params) {
556
+ const queryParams = {};
557
+ if (params) {
558
+ if (params.namespaceName !== void 0) queryParams["namespaceName"] = params.namespaceName;
559
+ if (params.extensionName !== void 0) queryParams["extensionName"] = params.extensionName;
560
+ if (params.extensionVersion !== void 0)
561
+ queryParams["extensionVersion"] = params.extensionVersion;
562
+ if (params.extensionId !== void 0) queryParams["extensionId"] = params.extensionId;
563
+ if (params.extensionUuid !== void 0) queryParams["extensionUuid"] = params.extensionUuid;
564
+ if (params.namespaceUuid !== void 0) queryParams["namespaceUuid"] = params.namespaceUuid;
565
+ if (params.includeAllVersions !== void 0)
566
+ queryParams["includeAllVersions"] = String(params.includeAllVersions);
567
+ if (params.targetPlatform !== void 0) queryParams["targetPlatform"] = params.targetPlatform;
568
+ if (params.size !== void 0) queryParams["size"] = String(params.size);
569
+ if (params.offset !== void 0) queryParams["offset"] = String(params.offset);
570
+ }
571
+ return this._get("/-/query", queryParams);
572
+ }
573
+ // -------------------------------------------------------------------------
574
+ // Extension metadata
575
+ // -------------------------------------------------------------------------
576
+ /**
577
+ * Get extension metadata (latest version or specific version).
578
+ *
579
+ * @param namespace - Publisher namespace (e.g., `'ms-python'`)
580
+ * @param name - Extension name (e.g., `'python'`)
581
+ * @param version - Specific version string (omit for latest)
582
+ * @param targetPlatform - Target platform filter
583
+ * @returns Full extension metadata
584
+ */
585
+ async getExtension(namespace, name, version, targetPlatform) {
586
+ let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
587
+ if (targetPlatform) {
588
+ path += `/${encodeURIComponent(targetPlatform)}`;
589
+ }
590
+ if (version) {
591
+ path += `/${encodeURIComponent(version)}`;
592
+ }
593
+ return this._get(path);
594
+ }
595
+ // -------------------------------------------------------------------------
596
+ // Namespace
597
+ // -------------------------------------------------------------------------
598
+ /**
599
+ * Get namespace information with extension listing.
600
+ *
601
+ * @param namespace - Namespace name (e.g., `'ms-python'`)
602
+ * @returns Namespace with map of extension name → API URL
603
+ */
604
+ async getNamespace(namespace) {
605
+ return this._get(`/${encodeURIComponent(namespace)}`);
606
+ }
607
+ /**
608
+ * Get namespace details including display name, social links, and extensions.
609
+ *
610
+ * @param namespace - Namespace name
611
+ * @returns Namespace details with social links and extension summaries
612
+ */
613
+ async getNamespaceDetails(namespace) {
614
+ return this._get(`/${encodeURIComponent(namespace)}/details`);
615
+ }
616
+ // -------------------------------------------------------------------------
617
+ // Versions
618
+ // -------------------------------------------------------------------------
619
+ /**
620
+ * List extension versions with pagination.
621
+ *
622
+ * @param namespace - Publisher namespace
623
+ * @param name - Extension name
624
+ * @param params - Pagination and platform filter options
625
+ * @returns Paginated version listing (version string → API URL)
626
+ */
627
+ async getVersions(namespace, name, params) {
628
+ let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
629
+ if (params?.targetPlatform) {
630
+ path += `/${encodeURIComponent(params.targetPlatform)}`;
631
+ }
632
+ path += "/versions";
633
+ const queryParams = {};
634
+ if (params?.size !== void 0) queryParams["size"] = String(params.size);
635
+ if (params?.offset !== void 0) queryParams["offset"] = String(params.offset);
636
+ return this._get(path, queryParams);
637
+ }
638
+ /**
639
+ * List version references with engine compatibility and file URLs.
640
+ *
641
+ * @param namespace - Publisher namespace
642
+ * @param name - Extension name
643
+ * @param params - Pagination and platform filter options
644
+ * @returns Paginated version references
645
+ */
646
+ async getVersionReferences(namespace, name, params) {
647
+ let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
648
+ if (params?.targetPlatform) {
649
+ path += `/${encodeURIComponent(params.targetPlatform)}`;
650
+ }
651
+ path += "/version-references";
652
+ const queryParams = {};
653
+ if (params?.size !== void 0) queryParams["size"] = String(params.size);
654
+ if (params?.offset !== void 0) queryParams["offset"] = String(params.offset);
655
+ return this._get(path, queryParams);
656
+ }
657
+ // -------------------------------------------------------------------------
658
+ // Reviews
659
+ // -------------------------------------------------------------------------
660
+ /**
661
+ * Get all reviews for an extension.
662
+ *
663
+ * @param namespace - Publisher namespace
664
+ * @param name - Extension name
665
+ * @returns Review list with all reviews (not paginated)
666
+ */
667
+ async getReviews(namespace, name) {
668
+ return this._get(
669
+ `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}/reviews`
670
+ );
671
+ }
672
+ // -------------------------------------------------------------------------
673
+ // URL builders (pure, no fetch)
674
+ // -------------------------------------------------------------------------
675
+ /**
676
+ * Build a VSIX download URL for a specific extension version.
677
+ *
678
+ * This is a pure URL builder — no network request is made.
679
+ *
680
+ * @param namespace - Publisher namespace
681
+ * @param name - Extension name
682
+ * @param version - Version string
683
+ * @param targetPlatform - Target platform (omit for universal)
684
+ * @returns Full download URL
685
+ */
686
+ getDownloadUrl(namespace, name, version, targetPlatform) {
687
+ let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
688
+ if (targetPlatform) {
689
+ path += `/${encodeURIComponent(targetPlatform)}`;
690
+ }
691
+ path += `/${encodeURIComponent(version)}/file/${encodeURIComponent(namespace)}.${encodeURIComponent(name)}-${encodeURIComponent(version)}.vsix`;
692
+ return `${this.baseUrl}${path}`;
693
+ }
694
+ /**
695
+ * Build a URL for accessing an extension file (README, icon, etc.).
696
+ *
697
+ * This is a pure URL builder — no network request is made.
698
+ *
699
+ * @param namespace - Publisher namespace
700
+ * @param name - Extension name
701
+ * @param version - Version string
702
+ * @param filePath - Relative file path within the extension
703
+ * @param targetPlatform - Target platform (omit for universal)
704
+ * @returns Full file URL
705
+ */
706
+ resolveFileUrl(namespace, name, version, filePath, targetPlatform) {
707
+ let path = `/${encodeURIComponent(namespace)}/${encodeURIComponent(name)}`;
708
+ if (targetPlatform) {
709
+ path += `/${encodeURIComponent(targetPlatform)}`;
710
+ }
711
+ const encodedFilePath = filePath.split("/").map(encodeURIComponent).join("/");
712
+ path += `/${encodeURIComponent(version)}/file/${encodedFilePath}`;
713
+ return `${this.baseUrl}${path}`;
714
+ }
715
+ // -------------------------------------------------------------------------
716
+ // Private helpers
717
+ // -------------------------------------------------------------------------
718
+ /** Build a full URL with optional query parameters. */
719
+ _buildUrl(path, params) {
720
+ const url = `${this.baseUrl}${path}`;
721
+ if (!params || Object.keys(params).length === 0) {
722
+ return url;
723
+ }
724
+ const searchParams = new URLSearchParams(params);
725
+ return `${url}?${searchParams.toString()}`;
726
+ }
727
+ /** Execute a GET request and return a typed result. */
728
+ async _get(path, params) {
729
+ const url = this._buildUrl(path, params);
730
+ let response;
731
+ try {
732
+ response = await this.fetch(url);
733
+ } catch (err) {
734
+ return {
735
+ ok: false,
736
+ statusCode: 0,
737
+ error: err instanceof Error ? err.message : "Network error"
738
+ };
739
+ }
740
+ if (!response.ok) {
741
+ let errorMessage = `HTTP ${response.status}`;
742
+ try {
743
+ const body = await response.json();
744
+ if (typeof body["error"] === "string") {
745
+ errorMessage = body["error"];
746
+ }
747
+ } catch {
748
+ }
749
+ return { ok: false, statusCode: response.status, error: errorMessage };
750
+ }
751
+ let data;
752
+ try {
753
+ data = await response.json();
754
+ } catch {
755
+ return { ok: false, statusCode: response.status, error: "Invalid JSON response" };
756
+ }
757
+ return { ok: true, data };
758
+ }
759
+ };
760
+
761
+ // src/federation-client.ts
762
+ var DEFAULT_CACHE_TTL_SECONDS = 3600;
763
+ function generateCacheKey(endpoint, params = {}) {
764
+ const sortedEntries = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
765
+ const paramString = sortedEntries.map(([k, v]) => `${k}=${v}`).join("&");
766
+ return `${endpoint}:${paramString}`;
767
+ }
768
+ function matchesFilter(extension, filter, contributionPoints) {
769
+ if (filter.blockedPublishers && filter.blockedPublishers.length > 0 && extension.namespace) {
770
+ const ns = extension.namespace.toLowerCase();
771
+ if (filter.blockedPublishers.some((bp) => bp.toLowerCase() === ns)) {
772
+ return { allowed: false, reason: `Publisher '${extension.namespace}' is blocked` };
773
+ }
774
+ }
775
+ if (filter.allowedCategories && filter.allowedCategories.length > 0) {
776
+ const categories = extension.categories ?? [];
777
+ const allowedLower = filter.allowedCategories.map((c) => c.toLowerCase());
778
+ const hasAllowedCategory = categories.some((cat) => allowedLower.includes(cat.toLowerCase()));
779
+ if (!hasAllowedCategory) {
780
+ return {
781
+ allowed: false,
782
+ reason: `No matching category (has: ${categories.join(", ")}; allowed: ${filter.allowedCategories.join(", ")})`
783
+ };
784
+ }
785
+ }
786
+ if (filter.declarativeOnly && contributionPoints) {
787
+ if (!isDeclarativeExtension(contributionPoints)) {
788
+ return {
789
+ allowed: false,
790
+ reason: "Extension uses non-declarative contribution points"
791
+ };
792
+ }
793
+ }
794
+ return { allowed: true };
795
+ }
796
+ function isDeclarativeExtension(contributionPoints) {
797
+ return contributionPoints.every((cp) => DECLARATIVE_CONTRIBUTION_POINTS.has(cp));
798
+ }
799
+ var FederationClient = class {
800
+ /**
801
+ * Create a new federation client.
802
+ *
803
+ * @param config - Federation configuration
804
+ * @param upstreamClient - Optional pre-configured OpenVSXClient (created from config if not provided)
805
+ */
806
+ constructor(config, upstreamClient) {
807
+ this.config = config;
808
+ this.cacheTtl = config.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS;
809
+ if (config.mode === "standalone") {
810
+ this.upstream = null;
811
+ } else if (upstreamClient) {
812
+ this.upstream = upstreamClient;
813
+ } else {
814
+ this.upstream = new OpenVSXClient(
815
+ config.upstream ? { baseUrl: config.upstream } : void 0
816
+ );
817
+ }
818
+ }
819
+ /**
820
+ * Search extensions with filters applied.
821
+ *
822
+ * In standalone mode, returns empty results. In proxy/mirror mode,
823
+ * delegates to upstream and applies filters.
824
+ */
825
+ async search(params) {
826
+ const cacheKey = this.getCacheKey("search", params ? this._searchParamsToRecord(params) : void 0);
827
+ if (!this.upstream) {
828
+ return {
829
+ ok: true,
830
+ data: {
831
+ extensions: [],
832
+ totalSize: 0,
833
+ filtered: 0,
834
+ source: "local",
835
+ cacheKey
836
+ }
837
+ };
838
+ }
839
+ const result = await this.upstream.search(params);
840
+ if (!result.ok) {
841
+ return result;
842
+ }
843
+ const { extensions, totalSize } = result.data;
844
+ const filter = this.config.filter;
845
+ if (filter) {
846
+ const passed = [];
847
+ let filteredCount = 0;
848
+ for (const ext of extensions) {
849
+ const extWithCategories = ext;
850
+ const categories = extWithCategories.categories;
851
+ const check = matchesFilter(
852
+ categories ? { categories, namespace: ext.namespace, name: ext.name } : { namespace: ext.namespace, name: ext.name },
853
+ filter
854
+ );
855
+ if (check.allowed) {
856
+ passed.push(ext);
857
+ } else {
858
+ filteredCount++;
859
+ }
860
+ }
861
+ return {
862
+ ok: true,
863
+ data: {
864
+ extensions: passed,
865
+ totalSize,
866
+ filtered: filteredCount,
867
+ source: "upstream",
868
+ cacheKey
869
+ }
870
+ };
871
+ }
872
+ return {
873
+ ok: true,
874
+ data: {
875
+ extensions,
876
+ totalSize,
877
+ filtered: 0,
878
+ source: "upstream",
879
+ cacheKey
880
+ }
881
+ };
882
+ }
883
+ /**
884
+ * Get a single extension with filter check.
885
+ *
886
+ * In standalone mode, returns null. In proxy/mirror mode,
887
+ * fetches from upstream and checks against filters.
888
+ */
889
+ async getExtension(namespace, name, version) {
890
+ const params = { namespace, name };
891
+ if (version) {
892
+ params["version"] = version;
893
+ }
894
+ const cacheKey = this.getCacheKey("extension", params);
895
+ if (!this.upstream) {
896
+ return {
897
+ ok: true,
898
+ data: {
899
+ extension: null,
900
+ filtered: false,
901
+ source: "local",
902
+ cacheKey
903
+ }
904
+ };
905
+ }
906
+ const result = await this.upstream.getExtension(namespace, name, version);
907
+ if (!result.ok) {
908
+ return result;
909
+ }
910
+ const ext = result.data;
911
+ const filter = this.config.filter;
912
+ if (filter) {
913
+ const check = matchesFilter(
914
+ {
915
+ categories: ext.categories,
916
+ namespace: ext.namespace,
917
+ name: ext.name
918
+ },
919
+ filter
920
+ );
921
+ if (!check.allowed) {
922
+ const data = {
923
+ extension: null,
924
+ filtered: true,
925
+ source: "upstream",
926
+ cacheKey
927
+ };
928
+ if (check.reason) {
929
+ data.filterReason = check.reason;
930
+ }
931
+ return { ok: true, data };
932
+ }
933
+ }
934
+ return {
935
+ ok: true,
936
+ data: {
937
+ extension: ext,
938
+ filtered: false,
939
+ source: "upstream",
940
+ cacheKey
941
+ }
942
+ };
943
+ }
944
+ /**
945
+ * Get a VSIX download URL for a specific extension version.
946
+ *
947
+ * Returns null in standalone mode (no upstream available).
948
+ */
949
+ getDownloadUrl(namespace, name, version, targetPlatform) {
950
+ if (!this.upstream) {
951
+ return null;
952
+ }
953
+ return this.upstream.getDownloadUrl(namespace, name, version, targetPlatform);
954
+ }
955
+ /**
956
+ * Build a cache key for a given endpoint and parameters.
957
+ */
958
+ getCacheKey(endpoint, params) {
959
+ const raw = generateCacheKey(endpoint, params);
960
+ let key = `federation:${this.config.mode}:${raw}`;
961
+ if (this.config.filter) {
962
+ const f = this.config.filter;
963
+ const parts = [];
964
+ if (f.declarativeOnly) parts.push("d=1");
965
+ if (f.allowedCategories && f.allowedCategories.length > 0) {
966
+ parts.push(`ac=${[...f.allowedCategories].sort().join(",")}`);
967
+ }
968
+ if (f.blockedPublishers && f.blockedPublishers.length > 0) {
969
+ parts.push(`bp=${[...f.blockedPublishers].sort().join(",")}`);
970
+ }
971
+ if (parts.length > 0) {
972
+ key += `:f(${parts.join(";")})`;
973
+ }
974
+ }
975
+ return { key, ttlSeconds: this.cacheTtl };
976
+ }
977
+ /**
978
+ * Get a readonly copy of the current federation configuration.
979
+ */
980
+ getConfig() {
981
+ const copy = { ...this.config };
982
+ if (this.config.filter) {
983
+ copy.filter = { ...this.config.filter };
984
+ if (this.config.filter.allowedCategories) {
985
+ copy.filter.allowedCategories = [...this.config.filter.allowedCategories];
986
+ }
987
+ if (this.config.filter.blockedPublishers) {
988
+ copy.filter.blockedPublishers = [...this.config.filter.blockedPublishers];
989
+ }
990
+ }
991
+ return copy;
992
+ }
993
+ // -------------------------------------------------------------------------
994
+ // Private helpers
995
+ // -------------------------------------------------------------------------
996
+ /** Convert search params to a plain Record for cache key generation. */
997
+ _searchParamsToRecord(params) {
998
+ const record = {};
999
+ if (params.query !== void 0) record["query"] = params.query;
1000
+ if (params.category !== void 0) record["category"] = params.category;
1001
+ if (params.targetPlatform !== void 0) record["targetPlatform"] = params.targetPlatform;
1002
+ if (params.size !== void 0) record["size"] = String(params.size);
1003
+ if (params.offset !== void 0) record["offset"] = String(params.offset);
1004
+ if (params.sortOrder !== void 0) record["sortOrder"] = params.sortOrder;
1005
+ if (params.sortBy !== void 0) record["sortBy"] = params.sortBy;
1006
+ if (params.includeAllVersions !== void 0)
1007
+ record["includeAllVersions"] = String(params.includeAllVersions);
1008
+ return record;
1009
+ }
1010
+ };
1011
+
1012
+ exports.DECLARATIVE_CONTRIBUTION_POINTS = DECLARATIVE_CONTRIBUTION_POINTS;
1013
+ exports.FederationClient = FederationClient;
1014
+ exports.OpenVSXClient = OpenVSXClient;
1015
+ exports.PLATFORM_SUPPORT = PLATFORM_SUPPORT;
1016
+ exports.SUPPORT_WEIGHTS = SUPPORT_WEIGHTS;
1017
+ exports.VsixScanError = VsixScanError;
1018
+ exports.buildCompatibilityReport = buildCompatibilityReport;
1019
+ exports.buildDependencyGraph = buildDependencyGraph;
1020
+ exports.classifyDeclarative = classifyDeclarative;
1021
+ exports.detectApiNamespaces = detectApiNamespaces;
1022
+ exports.enumerateContributionPoints = enumerateContributionPoints;
1023
+ exports.extractDependencyNode = extractDependencyNode;
1024
+ exports.flowStateToVsCode = flowStateToVsCode;
1025
+ exports.generateCacheKey = generateCacheKey;
1026
+ exports.generateRecommendations = generateRecommendations;
1027
+ exports.getPlatformSupport = getPlatformSupport;
1028
+ exports.isDeclarativeExtension = isDeclarativeExtension;
1029
+ exports.mapActivationEvents = mapActivationEvents;
1030
+ exports.mapCategory = mapCategory;
1031
+ exports.mapExtensionKind = mapExtensionKind;
1032
+ exports.matchesFilter = matchesFilter;
1033
+ exports.normalizeExtensionId = normalizeExtensionId;
1034
+ exports.resolveDependencies = resolveDependencies;
1035
+ exports.reverseActivationEvents = reverseActivationEvents;
1036
+ exports.reverseCategory = reverseCategory;
1037
+ exports.reverseExtensionKind = reverseExtensionKind;
1038
+ exports.scanVsix = scanVsix;
1039
+ exports.scoreExtension = scoreExtension;
1040
+ exports.vsCodeToFlowState = vsCodeToFlowState;
1041
+ //# sourceMappingURL=index.js.map
1042
+ //# sourceMappingURL=index.js.map