registryx-server 0.1.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/README.md +203 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/adapters/crates.ts.html +847 -0
- package/coverage/lcov-report/src/adapters/index.html +176 -0
- package/coverage/lcov-report/src/adapters/index.ts.html +97 -0
- package/coverage/lcov-report/src/adapters/maven.ts.html +637 -0
- package/coverage/lcov-report/src/adapters/npm.ts.html +817 -0
- package/coverage/lcov-report/src/adapters/pypi.ts.html +730 -0
- package/coverage/lcov-report/src/cache.ts.html +202 -0
- package/coverage/lcov-report/src/config.ts.html +208 -0
- package/coverage/lcov-report/src/index.html +161 -0
- package/coverage/lcov-report/src/server.ts.html +1339 -0
- package/coverage/lcov-report/src/types.ts.html +373 -0
- package/coverage/lcov-report/src/utils/fetch.ts.html +220 -0
- package/coverage/lcov-report/src/utils/format.ts.html +130 -0
- package/coverage/lcov-report/src/utils/index.html +131 -0
- package/coverage/lcov.info +1686 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1210 -0
- package/eslint.config.mjs +16 -0
- package/package.json +41 -0
- package/src/adapters/crates.ts +254 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/maven.ts +184 -0
- package/src/adapters/npm.ts +244 -0
- package/src/adapters/pypi.ts +215 -0
- package/src/cache.ts +39 -0
- package/src/config.ts +41 -0
- package/src/index.ts +25 -0
- package/src/server.ts +418 -0
- package/src/types.ts +96 -0
- package/src/utils/fetch.ts +45 -0
- package/src/utils/format.ts +15 -0
- package/test/adapters.test.ts +575 -0
- package/test/cache.test.ts +47 -0
- package/test/config.test.ts +69 -0
- package/test/fetch.test.ts +51 -0
- package/test/format.test.ts +35 -0
- package/test/server.test.ts +23 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import tseslint from 'typescript-eslint';
|
|
2
|
+
import security from 'eslint-plugin-security';
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
...tseslint.configs.recommended,
|
|
6
|
+
security.configs.recommended,
|
|
7
|
+
{
|
|
8
|
+
files: ['src/**/*.ts', 'test/**/*.ts'],
|
|
9
|
+
rules: {
|
|
10
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
11
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
12
|
+
'security/detect-object-injection': 'off',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{ ignores: ['dist/', 'node_modules/', 'coverage/'] },
|
|
16
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "registryx-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server providing unified access to npm, PyPI, Maven Central, and crates.io package registries",
|
|
5
|
+
"readme": "README.md",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"registryx-server": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"lint": "eslint src/ test/",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"format": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
19
|
+
"ci": "tsc --noEmit && eslint src/ test/ && vitest run --coverage && prettier --check \"src/**/*.ts\" \"test/**/*.ts\""
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
23
|
+
"zod": "^3.23.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.0.0",
|
|
27
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
28
|
+
"eslint": "^9.0.0",
|
|
29
|
+
"eslint-plugin-security": "^3.0.0",
|
|
30
|
+
"prettier": "^3.0.0",
|
|
31
|
+
"tsup": "^8.0.0",
|
|
32
|
+
"tsx": "^4.19.0",
|
|
33
|
+
"typescript": "^5.0.0",
|
|
34
|
+
"typescript-eslint": "^8.0.0",
|
|
35
|
+
"vitest": "^2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import type { Cache } from '../cache.js';
|
|
3
|
+
import type {
|
|
4
|
+
RegistryAdapter,
|
|
5
|
+
PackageSearchResult,
|
|
6
|
+
PackageMetadata,
|
|
7
|
+
VersionInfo,
|
|
8
|
+
VersionDetail,
|
|
9
|
+
Dependency,
|
|
10
|
+
DownloadStats,
|
|
11
|
+
MaintainerInfo,
|
|
12
|
+
PackageHealth,
|
|
13
|
+
SecurityAdvisory,
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
import { registryFetch } from '../utils/fetch.js';
|
|
16
|
+
|
|
17
|
+
export class CratesAdapter implements RegistryAdapter {
|
|
18
|
+
readonly name = 'crates' as const;
|
|
19
|
+
constructor(
|
|
20
|
+
private config: Config,
|
|
21
|
+
private cache: Cache
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async search(query: string, limit: number): Promise<PackageSearchResult[]> {
|
|
25
|
+
const key = `crates:search:${query}:${limit}`;
|
|
26
|
+
const cached = this.cache.get<PackageSearchResult[]>(key);
|
|
27
|
+
if (cached) return cached;
|
|
28
|
+
|
|
29
|
+
const url = `https://crates.io/api/v1/crates?q=${encodeURIComponent(query)}&per_page=${limit}`;
|
|
30
|
+
const res = await registryFetch(url, this.config);
|
|
31
|
+
const data = (await res.json()) as any;
|
|
32
|
+
|
|
33
|
+
const results: PackageSearchResult[] = (data.crates ?? []).map(
|
|
34
|
+
(c: { name: string; max_version: string; description?: string; downloads: number }) => ({
|
|
35
|
+
name: c.name,
|
|
36
|
+
version: c.max_version,
|
|
37
|
+
description: c.description ?? '',
|
|
38
|
+
downloads: `${c.downloads} total`,
|
|
39
|
+
registry: 'crates' as const,
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
this.cache.set(key, results);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getPackage(name: string): Promise<PackageMetadata> {
|
|
47
|
+
const key = `crates:pkg:${name}`;
|
|
48
|
+
const cached = this.cache.get<PackageMetadata>(key);
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
|
|
51
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
|
|
52
|
+
const res = await registryFetch(url, this.config);
|
|
53
|
+
const data = (await res.json()) as any;
|
|
54
|
+
const crate = data.crate;
|
|
55
|
+
if (!crate) throw new Error(`Crate not found: ${name}`);
|
|
56
|
+
|
|
57
|
+
const result: PackageMetadata = {
|
|
58
|
+
name: crate.name,
|
|
59
|
+
version: crate.max_version ?? '',
|
|
60
|
+
description: crate.description ?? '',
|
|
61
|
+
license: '',
|
|
62
|
+
homepage: crate.homepage ?? '',
|
|
63
|
+
repository: crate.repository ?? '',
|
|
64
|
+
keywords: crate.keywords ?? [],
|
|
65
|
+
registry: 'crates',
|
|
66
|
+
};
|
|
67
|
+
this.cache.set(key, result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getReadme(name: string): Promise<string> {
|
|
72
|
+
try {
|
|
73
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
|
|
74
|
+
const res = await registryFetch(url, this.config);
|
|
75
|
+
const data = (await res.json()) as any;
|
|
76
|
+
// crates.io doesn't return readme in main endpoint; link to it
|
|
77
|
+
return data.crate?.readme ?? `See https://crates.io/crates/${name}`;
|
|
78
|
+
} catch {
|
|
79
|
+
return 'README not available.';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getMaintainers(name: string): Promise<MaintainerInfo[]> {
|
|
84
|
+
try {
|
|
85
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/owner_user`;
|
|
86
|
+
const res = await registryFetch(url, this.config);
|
|
87
|
+
const data = (await res.json()) as any;
|
|
88
|
+
return (data.users ?? []).map((u: { login: string; name?: string; url?: string }) => ({
|
|
89
|
+
name: u.name ?? u.login,
|
|
90
|
+
url: u.url,
|
|
91
|
+
}));
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async listVersions(name: string, limit: number, stableOnly: boolean): Promise<VersionInfo[]> {
|
|
98
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/versions`;
|
|
99
|
+
const res = await registryFetch(url, this.config);
|
|
100
|
+
const data = (await res.json()) as any;
|
|
101
|
+
|
|
102
|
+
let results: VersionInfo[] = (data.versions ?? []).map(
|
|
103
|
+
(v: { num: string; created_at: string }) => ({
|
|
104
|
+
version: v.num,
|
|
105
|
+
date: v.created_at,
|
|
106
|
+
prerelease: /[-]/.test(v.num.replace(/^\d+\.\d+\.\d+/, '')),
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (stableOnly) results = results.filter((v) => !v.prerelease);
|
|
111
|
+
return results.slice(0, limit);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getVersion(name: string, version: string): Promise<VersionDetail> {
|
|
115
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/${version}`;
|
|
116
|
+
const res = await registryFetch(url, this.config);
|
|
117
|
+
const data = (await res.json()) as any;
|
|
118
|
+
const v = data.version;
|
|
119
|
+
if (!v) throw new Error(`Version not found: ${name}@${version}`);
|
|
120
|
+
|
|
121
|
+
const depUrl = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/${version}/dependencies`;
|
|
122
|
+
let deps: Dependency[] = [];
|
|
123
|
+
try {
|
|
124
|
+
const depRes = await registryFetch(depUrl, this.config);
|
|
125
|
+
const depData = (await depRes.json()) as any;
|
|
126
|
+
deps = (depData.dependencies ?? []).map(
|
|
127
|
+
(d: { crate_id: string; req: string; kind: string }) => ({
|
|
128
|
+
name: d.crate_id,
|
|
129
|
+
version: d.req,
|
|
130
|
+
type: d.kind === 'dev' ? ('dev' as const) : ('runtime' as const),
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
} catch {
|
|
134
|
+
// Dependencies endpoint might fail
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: v.crate ?? name,
|
|
139
|
+
version: v.num ?? version,
|
|
140
|
+
date: v.created_at ?? '',
|
|
141
|
+
license: v.license ?? 'Unknown',
|
|
142
|
+
size: v.crate_size ? `${Math.round(v.crate_size / 1024)}KB` : 'Unknown',
|
|
143
|
+
dependencies: deps,
|
|
144
|
+
registry: 'crates',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getDependencies(
|
|
149
|
+
name: string,
|
|
150
|
+
version: string,
|
|
151
|
+
type: 'runtime' | 'dev' | 'all'
|
|
152
|
+
): Promise<Dependency[]> {
|
|
153
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/${version}/dependencies`;
|
|
154
|
+
const res = await registryFetch(url, this.config);
|
|
155
|
+
const data = (await res.json()) as any;
|
|
156
|
+
|
|
157
|
+
let deps: Dependency[] = (data.dependencies ?? []).map(
|
|
158
|
+
(d: { crate_id: string; req: string; kind: string }) => ({
|
|
159
|
+
name: d.crate_id,
|
|
160
|
+
version: d.req,
|
|
161
|
+
type: d.kind === 'dev' ? ('dev' as const) : ('runtime' as const),
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (type !== 'all') {
|
|
166
|
+
deps = deps.filter((d) => d.type === type);
|
|
167
|
+
}
|
|
168
|
+
return deps;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getReverseDependencies(name: string, limit: number): Promise<PackageSearchResult[]> {
|
|
172
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/reverse_dependencies?per_page=${limit}`;
|
|
173
|
+
const res = await registryFetch(url, this.config);
|
|
174
|
+
const data = (await res.json()) as any;
|
|
175
|
+
|
|
176
|
+
return (data.versions ?? []).map((v: { crate: string; num: string }) => ({
|
|
177
|
+
name: v.crate ?? '',
|
|
178
|
+
version: v.num ?? '',
|
|
179
|
+
description: '',
|
|
180
|
+
downloads: '',
|
|
181
|
+
registry: 'crates' as const,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async getDownloadStats(name: string, period: string): Promise<DownloadStats> {
|
|
186
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
|
|
187
|
+
const res = await registryFetch(url, this.config);
|
|
188
|
+
const data = (await res.json()) as any;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
name,
|
|
192
|
+
registry: 'crates',
|
|
193
|
+
period,
|
|
194
|
+
total: data.crate?.downloads ?? 0,
|
|
195
|
+
breakdown: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getPackageHealth(name: string): Promise<PackageHealth> {
|
|
200
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
|
|
201
|
+
const res = await registryFetch(url, this.config);
|
|
202
|
+
const data = (await res.json()) as any;
|
|
203
|
+
const crate = data.crate ?? {};
|
|
204
|
+
|
|
205
|
+
const signals: string[] = [];
|
|
206
|
+
if (crate.documentation) signals.push('Has documentation');
|
|
207
|
+
if (crate.repository) signals.push('Has repository');
|
|
208
|
+
if (crate.homepage) signals.push('Has homepage');
|
|
209
|
+
if (crate.description) signals.push('Has description');
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
registry: 'crates',
|
|
214
|
+
score: Math.min(10, signals.length * 2 + 3),
|
|
215
|
+
lastPublish: crate.updated_at ?? '',
|
|
216
|
+
weeklyDownloads: crate.recent_downloads,
|
|
217
|
+
dependencyCount: 0,
|
|
218
|
+
hasTests: false,
|
|
219
|
+
hasTypings: false,
|
|
220
|
+
signals,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getSecurityAdvisories(name: string, _version?: string): Promise<SecurityAdvisory[]> {
|
|
225
|
+
try {
|
|
226
|
+
const url = 'https://osv.dev/v1/query';
|
|
227
|
+
const body = { package: { name, ecosystem: 'crates.io' } };
|
|
228
|
+
const res = await fetch(url, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: { 'Content-Type': 'application/json' },
|
|
231
|
+
body: JSON.stringify(body),
|
|
232
|
+
signal: AbortSignal.timeout(this.config.timeoutMs),
|
|
233
|
+
});
|
|
234
|
+
if (!res.ok) return [];
|
|
235
|
+
const data = (await res.json()) as any;
|
|
236
|
+
return (data.vulns ?? []).map(
|
|
237
|
+
(v: {
|
|
238
|
+
id: string;
|
|
239
|
+
summary?: string;
|
|
240
|
+
severity?: { score: string }[];
|
|
241
|
+
references?: { url: string }[];
|
|
242
|
+
}) => ({
|
|
243
|
+
id: v.id,
|
|
244
|
+
title: v.summary ?? v.id,
|
|
245
|
+
severity: v.severity?.[0]?.score ?? 'Unknown',
|
|
246
|
+
url: v.references?.[0]?.url ?? `https://osv.dev/vulnerability/${v.id}`,
|
|
247
|
+
affectedVersions: 'See advisory',
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
} catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Config } from '../config.js';
|
|
2
|
+
import type { Cache } from '../cache.js';
|
|
3
|
+
import type {
|
|
4
|
+
RegistryAdapter,
|
|
5
|
+
PackageSearchResult,
|
|
6
|
+
PackageMetadata,
|
|
7
|
+
VersionInfo,
|
|
8
|
+
VersionDetail,
|
|
9
|
+
Dependency,
|
|
10
|
+
DownloadStats,
|
|
11
|
+
MaintainerInfo,
|
|
12
|
+
PackageHealth,
|
|
13
|
+
SecurityAdvisory,
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
import { registryFetch } from '../utils/fetch.js';
|
|
16
|
+
|
|
17
|
+
export class MavenAdapter implements RegistryAdapter {
|
|
18
|
+
readonly name = 'maven' as const;
|
|
19
|
+
constructor(
|
|
20
|
+
private config: Config,
|
|
21
|
+
private cache: Cache
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
private parseCoords(name: string): { groupId: string; artifactId: string } {
|
|
25
|
+
const parts = name.split(':');
|
|
26
|
+
if (parts.length !== 2)
|
|
27
|
+
throw new Error(`Maven package must be groupId:artifactId, got: ${name}`);
|
|
28
|
+
return { groupId: parts[0], artifactId: parts[1] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async search(query: string, limit: number): Promise<PackageSearchResult[]> {
|
|
32
|
+
const key = `maven:search:${query}:${limit}`;
|
|
33
|
+
const cached = this.cache.get<PackageSearchResult[]>(key);
|
|
34
|
+
if (cached) return cached;
|
|
35
|
+
|
|
36
|
+
const url = `https://search.maven.org/solrsearch/select?q=${encodeURIComponent(query)}&rows=${limit}&wt=json`;
|
|
37
|
+
const res = await registryFetch(url, this.config);
|
|
38
|
+
const data = (await res.json()) as any;
|
|
39
|
+
|
|
40
|
+
const results: PackageSearchResult[] = (data.response?.docs ?? []).map(
|
|
41
|
+
(doc: { g: string; a: string; latestVersion: string; p?: string }) => ({
|
|
42
|
+
name: `${doc.g}:${doc.a}`,
|
|
43
|
+
version: doc.latestVersion ?? '',
|
|
44
|
+
description: doc.p ?? '',
|
|
45
|
+
downloads: '',
|
|
46
|
+
registry: 'maven' as const,
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
this.cache.set(key, results);
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getPackage(name: string): Promise<PackageMetadata> {
|
|
54
|
+
const key = `maven:pkg:${name}`;
|
|
55
|
+
const cached = this.cache.get<PackageMetadata>(key);
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
|
|
58
|
+
const { groupId, artifactId } = this.parseCoords(name);
|
|
59
|
+
const url = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}&rows=1&wt=json`;
|
|
60
|
+
const res = await registryFetch(url, this.config);
|
|
61
|
+
const data = (await res.json()) as any;
|
|
62
|
+
const doc = data.response?.docs?.[0];
|
|
63
|
+
if (!doc) throw new Error(`Package not found: ${name}`);
|
|
64
|
+
|
|
65
|
+
const result: PackageMetadata = {
|
|
66
|
+
name: `${doc.g}:${doc.a}`,
|
|
67
|
+
version: doc.latestVersion ?? '',
|
|
68
|
+
description: doc.p ?? '',
|
|
69
|
+
license: '',
|
|
70
|
+
homepage: '',
|
|
71
|
+
repository: '',
|
|
72
|
+
keywords: [],
|
|
73
|
+
registry: 'maven',
|
|
74
|
+
};
|
|
75
|
+
this.cache.set(key, result);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getReadme(_name: string): Promise<string> {
|
|
80
|
+
return 'Maven Central does not provide README content via API.';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getMaintainers(_name: string): Promise<MaintainerInfo[]> {
|
|
84
|
+
return []; // Maven Central API doesn't expose maintainer info easily
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async listVersions(name: string, limit: number, stableOnly: boolean): Promise<VersionInfo[]> {
|
|
88
|
+
const { groupId, artifactId } = this.parseCoords(name);
|
|
89
|
+
const url = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}&core=gav&rows=${limit * 2}&wt=json`;
|
|
90
|
+
const res = await registryFetch(url, this.config);
|
|
91
|
+
const data = (await res.json()) as any;
|
|
92
|
+
|
|
93
|
+
let results: VersionInfo[] = (data.response?.docs ?? []).map(
|
|
94
|
+
(doc: { v: string; timestamp: number }) => ({
|
|
95
|
+
version: doc.v,
|
|
96
|
+
date: doc.timestamp ? new Date(doc.timestamp).toISOString() : '',
|
|
97
|
+
prerelease: /[-.](?:alpha|beta|rc|snapshot|milestone|m\d)/i.test(doc.v),
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (stableOnly) results = results.filter((v) => !v.prerelease);
|
|
102
|
+
return results.slice(0, limit);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async getVersion(name: string, version: string): Promise<VersionDetail> {
|
|
106
|
+
const { groupId, artifactId } = this.parseCoords(name);
|
|
107
|
+
const url = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}+AND+v:${encodeURIComponent(version)}&rows=1&wt=json`;
|
|
108
|
+
const res = await registryFetch(url, this.config);
|
|
109
|
+
const data = (await res.json()) as any;
|
|
110
|
+
const doc = data.response?.docs?.[0];
|
|
111
|
+
if (!doc) throw new Error(`Version not found: ${name}@${version}`);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
name: `${doc.g}:${doc.a}`,
|
|
115
|
+
version: doc.v ?? version,
|
|
116
|
+
date: doc.timestamp ? new Date(doc.timestamp).toISOString() : '',
|
|
117
|
+
license: '',
|
|
118
|
+
size: 'Unknown',
|
|
119
|
+
dependencies: [],
|
|
120
|
+
registry: 'maven',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getDependencies(
|
|
125
|
+
_name: string,
|
|
126
|
+
_version: string,
|
|
127
|
+
_type: 'runtime' | 'dev' | 'all'
|
|
128
|
+
): Promise<Dependency[]> {
|
|
129
|
+
return []; // Maven Central search API doesn't expose dependency info
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getReverseDependencies(_name: string, _limit: number): Promise<PackageSearchResult[]> {
|
|
133
|
+
return []; // Not available via search API
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getDownloadStats(name: string, period: string): Promise<DownloadStats> {
|
|
137
|
+
return { name, registry: 'maven', period, total: 0, breakdown: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async getPackageHealth(name: string): Promise<PackageHealth> {
|
|
141
|
+
return {
|
|
142
|
+
name,
|
|
143
|
+
registry: 'maven',
|
|
144
|
+
score: 5,
|
|
145
|
+
lastPublish: '',
|
|
146
|
+
dependencyCount: 0,
|
|
147
|
+
hasTests: false,
|
|
148
|
+
hasTypings: false,
|
|
149
|
+
signals: ['Available on Maven Central'],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getSecurityAdvisories(name: string, _version?: string): Promise<SecurityAdvisory[]> {
|
|
154
|
+
try {
|
|
155
|
+
const { groupId, artifactId } = this.parseCoords(name);
|
|
156
|
+
const url = 'https://osv.dev/v1/query';
|
|
157
|
+
const body = { package: { name: `${groupId}:${artifactId}`, ecosystem: 'Maven' } };
|
|
158
|
+
const res = await fetch(url, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify(body),
|
|
162
|
+
signal: AbortSignal.timeout(this.config.timeoutMs),
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) return [];
|
|
165
|
+
const data = (await res.json()) as any;
|
|
166
|
+
return (data.vulns ?? []).map(
|
|
167
|
+
(v: {
|
|
168
|
+
id: string;
|
|
169
|
+
summary?: string;
|
|
170
|
+
severity?: { score: string }[];
|
|
171
|
+
references?: { url: string }[];
|
|
172
|
+
}) => ({
|
|
173
|
+
id: v.id,
|
|
174
|
+
title: v.summary ?? v.id,
|
|
175
|
+
severity: v.severity?.[0]?.score ?? 'Unknown',
|
|
176
|
+
url: v.references?.[0]?.url ?? `https://osv.dev/vulnerability/${v.id}`,
|
|
177
|
+
affectedVersions: 'See advisory',
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|