@4da/mcp-server 6.1.0 → 6.2.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/CHANGELOG.md +168 -0
- package/README.md +14 -9
- package/dist/live/changelog-classify.d.ts +9 -3
- package/dist/live/changelog-classify.js +122 -12
- package/dist/live/changelog.d.ts +8 -4
- package/dist/live/changelog.js +67 -29
- package/dist/live/fix-paths.js +1 -1
- package/dist/live/github-releases.d.ts +69 -0
- package/dist/live/github-releases.js +252 -0
- package/dist/live/go-releases.d.ts +48 -0
- package/dist/live/go-releases.js +196 -0
- package/dist/live/index.d.ts +4 -5
- package/dist/live/index.js +7 -1
- package/dist/live/osv-ranges.d.ts +6 -0
- package/dist/live/osv-ranges.js +3 -0
- package/dist/live/package-archive.d.ts +2 -0
- package/dist/live/package-archive.js +7 -4
- package/dist/live/pypi-releases.d.ts +44 -0
- package/dist/live/pypi-releases.js +103 -0
- package/dist/live/release-metadata.d.ts +17 -8
- package/dist/live/release-metadata.js +6 -3
- package/dist/live/upgrade-go.d.ts +62 -0
- package/dist/live/upgrade-go.js +130 -0
- package/dist/live/upgrade-pypi.d.ts +42 -0
- package/dist/live/upgrade-pypi.js +85 -0
- package/dist/live/upgrade-sources.d.ts +28 -11
- package/dist/live/upgrade-sources.js +86 -19
- package/dist/live/zip-archive.d.ts +22 -0
- package/dist/live/zip-archive.js +98 -0
- package/dist/schema-registry.js +3 -3
- package/dist/server-instructions.d.ts +6 -0
- package/dist/server-instructions.js +20 -15
- package/dist/tools/dependency-check-signals.d.ts +16 -5
- package/dist/tools/dependency-check-signals.js +46 -18
- package/dist/tools/dependency-check-versions.d.ts +94 -0
- package/dist/tools/dependency-check-versions.js +213 -0
- package/dist/tools/dependency-check.d.ts +26 -10
- package/dist/tools/dependency-check.js +321 -74
- package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
- package/dist/tools/upgrade-impact-callsites.js +23 -5
- package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
- package/dist/tools/upgrade-impact-changelog.js +79 -0
- package/dist/tools/upgrade-impact-imports.d.ts +42 -0
- package/dist/tools/upgrade-impact-imports.js +260 -0
- package/dist/tools/upgrade-impact-report.d.ts +13 -0
- package/dist/tools/upgrade-impact-report.js +13 -4
- package/dist/tools/upgrade-impact.d.ts +13 -4
- package/dist/tools/upgrade-impact.js +235 -84
- package/dist/tools/what-should-i-know.js +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Go modules for `upgrade_impact`, from the module proxy (proxy.golang.org):
|
|
4
|
+
* `/@v/list` for the tagged versions, `/@v/<version>.info` for a release's
|
|
5
|
+
* time and VCS origin, and `/@v/<version>.zip` as the archive a changelog is
|
|
6
|
+
* read from (the zip holds the module's files under `<module>@<version>/`).
|
|
7
|
+
*
|
|
8
|
+
* Major versions 2 and up are DIFFERENT modules: `github.com/jackc/pgx/v5`
|
|
9
|
+
* is not a newer release of `github.com/jackc/pgx/v4`, and the proxy lists
|
|
10
|
+
* each path's own versions only. `goMajorPath` gives the path a major lives
|
|
11
|
+
* at, so an upgrade across majors is read from the target module and the
|
|
12
|
+
* report can say that every import path changes.
|
|
13
|
+
*
|
|
14
|
+
* `.info` carries `Origin` (VCS URL, the tag's ref, the module's subdirectory
|
|
15
|
+
* in a multi-module repository) for versions the proxy fetched recently.
|
|
16
|
+
* That, not a guess from the import path, names the GitHub repository and tag
|
|
17
|
+
* for the opt-in release-notes fallback: `go.uber.org/zap` lives at
|
|
18
|
+
* github.com/uber-go/zap, and aws-sdk-go-v2's `service/s3` tags its releases
|
|
19
|
+
* `service/s3/v1.50.0`.
|
|
20
|
+
*/
|
|
21
|
+
const GO_PROXY = "https://proxy.golang.org";
|
|
22
|
+
const GO_TIMEOUT_MS = 15_000;
|
|
23
|
+
/** `.info` reads per call: one per release crossed, newest first, then dates are left null. */
|
|
24
|
+
const MAX_INFO_READS = 40;
|
|
25
|
+
/** A module path: a dotted host, then path elements (case kept; the proxy escapes capitals). */
|
|
26
|
+
export const GO_MODULE = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}(?:\/[A-Za-z0-9._~+-]+)+$|^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/;
|
|
27
|
+
/** Proxy escaping: each capital letter becomes `!` + lowercase. */
|
|
28
|
+
export function escapeGoPath(path) {
|
|
29
|
+
return path.replace(/[A-Z]/g, (c) => `!${c.toLowerCase()}`);
|
|
30
|
+
}
|
|
31
|
+
/** The major version a module path carries: `/v5` -> 5, `gopkg.in/yaml.v3` -> 3, none -> null (0 or 1). */
|
|
32
|
+
export function goPathMajor(path) {
|
|
33
|
+
const m = /\/v(\d+)$/.exec(path) ?? (/^gopkg\.in\//.test(path) ? /\.v(\d+)$/.exec(path) : null);
|
|
34
|
+
return m ? Number(m[1]) : null;
|
|
35
|
+
}
|
|
36
|
+
/** The module path without its major suffix. */
|
|
37
|
+
export function goBasePath(path) {
|
|
38
|
+
if (/^gopkg\.in\//.test(path))
|
|
39
|
+
return path.replace(/\.v\d+$/, "");
|
|
40
|
+
return path.replace(/\/v\d+$/, "");
|
|
41
|
+
}
|
|
42
|
+
/** The path major `major` lives at: none for 0 and 1, `/vN` (or gopkg.in's `.vN`) from 2. */
|
|
43
|
+
export function goMajorPath(path, major) {
|
|
44
|
+
const base = goBasePath(path);
|
|
45
|
+
if (/^gopkg\.in\//.test(path))
|
|
46
|
+
return `${base}.v${major}`;
|
|
47
|
+
return major >= 2 ? `${base}/v${major}` : base;
|
|
48
|
+
}
|
|
49
|
+
export function goVersionMajor(version) {
|
|
50
|
+
const m = /^v?(\d+)\./.exec(version.trim());
|
|
51
|
+
return m ? Number(m[1]) : null;
|
|
52
|
+
}
|
|
53
|
+
/** GitHub repository from a module path: `github.com/<owner>/<repo>/...`. */
|
|
54
|
+
export function githubRepoOfGoPath(path) {
|
|
55
|
+
const m = /^github\.com\/([\w.-]+)\/([\w.-]+)/.exec(path);
|
|
56
|
+
return m ? `https://github.com/${m[1]}/${m[2]}` : null;
|
|
57
|
+
}
|
|
58
|
+
export async function goInfo(net, module, version) {
|
|
59
|
+
const res = await net.fetch(`${GO_PROXY}/${escapeGoPath(module)}/@v/${escapeGoPath(version)}.info`, {}, GO_TIMEOUT_MS);
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
return null;
|
|
62
|
+
try {
|
|
63
|
+
return (await res.json());
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function loadGoIndex(net, module) {
|
|
70
|
+
const base = `${GO_PROXY}/${escapeGoPath(module)}/@v/list`;
|
|
71
|
+
const res = await net.fetch(base, {}, GO_TIMEOUT_MS);
|
|
72
|
+
// The proxy answers 404 and 410 for a path it cannot resolve.
|
|
73
|
+
if (res.status === 404 || res.status === 410)
|
|
74
|
+
return null;
|
|
75
|
+
if (!res.ok)
|
|
76
|
+
throw new Error(`Go module proxy HTTP ${res.status} for ${module}`);
|
|
77
|
+
const listed = (await res.text())
|
|
78
|
+
.split("\n")
|
|
79
|
+
.map((v) => v.trim())
|
|
80
|
+
.filter((v) => /^v\d+\.\d+\.\d+/.test(v));
|
|
81
|
+
let versions = listed.map((version) => ({ version, published: null }));
|
|
82
|
+
if (versions.length === 0) {
|
|
83
|
+
// An untagged module: the proxy's @latest is a pseudo-version.
|
|
84
|
+
const latest = await net.fetch(`${GO_PROXY}/${escapeGoPath(module)}/@latest`, {}, GO_TIMEOUT_MS);
|
|
85
|
+
if (!latest.ok)
|
|
86
|
+
return null;
|
|
87
|
+
const info = (await latest.json());
|
|
88
|
+
if (!info.Version)
|
|
89
|
+
return null;
|
|
90
|
+
versions = [{ version: info.Version, published: info.Time ?? null }];
|
|
91
|
+
}
|
|
92
|
+
return { name: module, ecosystem: "Go", versions, repository: githubRepoOfGoPath(module) };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Publish times for the versions an upgrade crosses (newest first, at most
|
|
96
|
+
* MAX_INFO_READS), and the target's VCS origin. Missing `.info` leaves a null date.
|
|
97
|
+
*/
|
|
98
|
+
export async function enrichGoVersions(net, module, versions, target, budget = MAX_INFO_READS) {
|
|
99
|
+
const rest = [...versions].reverse().filter((v) => v !== target && !v.published);
|
|
100
|
+
const wanted = (target ? [target, ...rest] : rest).slice(0, budget);
|
|
101
|
+
let origin = null;
|
|
102
|
+
await Promise.all(wanted.map(async (v) => {
|
|
103
|
+
const info = await goInfo(net, module, v.version).catch(() => null);
|
|
104
|
+
if (info?.Time && !v.published)
|
|
105
|
+
v.published = info.Time;
|
|
106
|
+
if (v === target && info?.Origin)
|
|
107
|
+
origin = info.Origin;
|
|
108
|
+
}));
|
|
109
|
+
return { origin };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The tag prefix a module's releases carry: `service/s3` for
|
|
113
|
+
* `refs/tags/service/s3/v1.50.0`, "" for a module at the repository root.
|
|
114
|
+
* From the proxy's recorded ref when there is one; otherwise from a
|
|
115
|
+
* github.com path (the elements after owner/repo, minus a `/vN` suffix,
|
|
116
|
+
* which a major-version subdirectory does not put in its tags).
|
|
117
|
+
*/
|
|
118
|
+
export function goTagPrefix(module, version, origin) {
|
|
119
|
+
const ref = origin?.Ref?.replace(/^refs\/tags\//, "");
|
|
120
|
+
if (ref && ref.endsWith(version))
|
|
121
|
+
return ref.slice(0, ref.length - version.length).replace(/\/$/, "");
|
|
122
|
+
if (origin?.Subdir !== undefined)
|
|
123
|
+
return origin.Subdir.replace(/(?:^|\/)v\d+$/, "");
|
|
124
|
+
const m = /^github\.com\/[\w.-]+\/[\w.-]+\/(.+)$/.exec(goBasePath(module));
|
|
125
|
+
return m ? m[1] : "";
|
|
126
|
+
}
|
|
127
|
+
/** The proxy's zip of one module version. */
|
|
128
|
+
export function goZipUrl(module, version) {
|
|
129
|
+
return `${GO_PROXY}/${escapeGoPath(module)}/@v/${escapeGoPath(version)}.zip`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PyPI for `upgrade_impact`: the version index from the PyPI JSON API
|
|
3
|
+
* (`https://pypi.org/pypi/<name>/json`), each release's sdist as the archive
|
|
4
|
+
* its changelog is read from (files.pythonhosted.org, PyPI's own file host),
|
|
5
|
+
* and the links the project itself publishes in `project_urls`.
|
|
6
|
+
*
|
|
7
|
+
* Versions are PEP 440, not semver (`2.0`, `3.0.0rc1`, `1.0.post1`), so they
|
|
8
|
+
* are ordered with version-compare.ts's PEP 440 comparator. A release's
|
|
9
|
+
* publish time is its earliest file upload; it is yanked when every file is.
|
|
10
|
+
*
|
|
11
|
+
* `project_urls` is metadata, not a changelog: a "Changelog" link usually
|
|
12
|
+
* points at an HTML docs page this server does not parse (it is reported as
|
|
13
|
+
* the release-notes URL). Only a link to a raw changelog FILE on GitHub can be
|
|
14
|
+
* read, and only when the GitHub fallback is enabled (github-releases.ts).
|
|
15
|
+
*/
|
|
16
|
+
import type { RegistryIndex, UpgradeNet } from "./upgrade-sources.js";
|
|
17
|
+
interface PyPIFile {
|
|
18
|
+
url?: string;
|
|
19
|
+
packagetype?: string;
|
|
20
|
+
filename?: string;
|
|
21
|
+
upload_time_iso_8601?: string;
|
|
22
|
+
yanked?: boolean;
|
|
23
|
+
yanked_reason?: string | null;
|
|
24
|
+
}
|
|
25
|
+
interface PyPIProject {
|
|
26
|
+
info?: {
|
|
27
|
+
name?: string;
|
|
28
|
+
home_page?: string | null;
|
|
29
|
+
project_urls?: Record<string, string> | null;
|
|
30
|
+
};
|
|
31
|
+
releases?: Record<string, PyPIFile[]>;
|
|
32
|
+
}
|
|
33
|
+
/** PEP 503 normalised name: lowercase, runs of `-`, `_`, `.` read as one `-`. */
|
|
34
|
+
export declare function normalizePypiName(name: string): string;
|
|
35
|
+
/** PEP 508 distribution name. */
|
|
36
|
+
export declare const PYPI_NAME: RegExp;
|
|
37
|
+
/** The project's source repository from `project_urls` / `home_page`: a GitHub URL first, then any https one. */
|
|
38
|
+
export declare function pypiRepository(info: PyPIProject["info"]): string | null;
|
|
39
|
+
/** Links the project labels as its changelog / release notes, in `project_urls` order. */
|
|
40
|
+
export declare function pypiChangelogLinks(info: PyPIProject["info"]): string[];
|
|
41
|
+
export declare function loadPypiIndex(net: UpgradeNet, name: string): Promise<RegistryIndex | null>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* PyPI for `upgrade_impact`: the version index from the PyPI JSON API
|
|
4
|
+
* (`https://pypi.org/pypi/<name>/json`), each release's sdist as the archive
|
|
5
|
+
* its changelog is read from (files.pythonhosted.org, PyPI's own file host),
|
|
6
|
+
* and the links the project itself publishes in `project_urls`.
|
|
7
|
+
*
|
|
8
|
+
* Versions are PEP 440, not semver (`2.0`, `3.0.0rc1`, `1.0.post1`), so they
|
|
9
|
+
* are ordered with version-compare.ts's PEP 440 comparator. A release's
|
|
10
|
+
* publish time is its earliest file upload; it is yanked when every file is.
|
|
11
|
+
*
|
|
12
|
+
* `project_urls` is metadata, not a changelog: a "Changelog" link usually
|
|
13
|
+
* points at an HTML docs page this server does not parse (it is reported as
|
|
14
|
+
* the release-notes URL). Only a link to a raw changelog FILE on GitHub can be
|
|
15
|
+
* read, and only when the GitHub fallback is enabled (github-releases.ts).
|
|
16
|
+
*/
|
|
17
|
+
import { parsePep440 } from "./version-compare.js";
|
|
18
|
+
const PYPI_TIMEOUT_MS = 30_000;
|
|
19
|
+
/** PEP 503 normalised name: lowercase, runs of `-`, `_`, `.` read as one `-`. */
|
|
20
|
+
export function normalizePypiName(name) {
|
|
21
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
22
|
+
}
|
|
23
|
+
/** PEP 508 distribution name. */
|
|
24
|
+
export const PYPI_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,198}[A-Za-z0-9])?$/;
|
|
25
|
+
/** The sdist to read a changelog from: a `.tar.gz` first, then a `.zip`. */
|
|
26
|
+
function pickSdist(files) {
|
|
27
|
+
const sdists = files.filter((f) => f.packagetype === "sdist" && typeof f.url === "string");
|
|
28
|
+
return (sdists.find((f) => /\.tar\.gz$/i.test(f.url)) ?? sdists.find((f) => /\.zip$/i.test(f.url)))?.url;
|
|
29
|
+
}
|
|
30
|
+
const REPO_KEY = /^(?:source(?: code)?|repository|repo|code|github|git|homepage|home|project)$/i;
|
|
31
|
+
const CHANGELOG_KEY = /change ?log|changes|release ?notes|releases|history|news|what'?s ?new/i;
|
|
32
|
+
/** The project's source repository from `project_urls` / `home_page`: a GitHub URL first, then any https one. */
|
|
33
|
+
export function pypiRepository(info) {
|
|
34
|
+
const urls = Object.entries(info?.project_urls ?? {}).filter(([, v]) => typeof v === "string");
|
|
35
|
+
const github = (v) => /^https?:\/\/(?:www\.)?github\.com\/[\w.-]+\/[\w.-]+/i.test(v);
|
|
36
|
+
const keyed = urls.filter(([k]) => REPO_KEY.test(k.trim()));
|
|
37
|
+
const pick = keyed.find(([, v]) => github(v))?.[1] ??
|
|
38
|
+
urls.find(([, v]) => github(v))?.[1] ??
|
|
39
|
+
(info?.home_page && github(info.home_page) ? info.home_page : null) ??
|
|
40
|
+
keyed.find(([, v]) => /^https:\/\//i.test(v))?.[1] ??
|
|
41
|
+
null;
|
|
42
|
+
return pick;
|
|
43
|
+
}
|
|
44
|
+
/** Links the project labels as its changelog / release notes, in `project_urls` order. */
|
|
45
|
+
export function pypiChangelogLinks(info) {
|
|
46
|
+
return Object.entries(info?.project_urls ?? {})
|
|
47
|
+
.filter(([k, v]) => typeof v === "string" && CHANGELOG_KEY.test(k) && /^https:\/\//i.test(v))
|
|
48
|
+
.map(([, v]) => v);
|
|
49
|
+
}
|
|
50
|
+
export async function loadPypiIndex(net, name) {
|
|
51
|
+
const url = `https://pypi.org/pypi/${encodeURIComponent(normalizePypiName(name))}/json`;
|
|
52
|
+
const res = await net.fetch(url, { headers: { Accept: "application/json" } }, PYPI_TIMEOUT_MS);
|
|
53
|
+
if (res.status === 404)
|
|
54
|
+
return null;
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw new Error(`PyPI HTTP ${res.status} for ${name}`);
|
|
57
|
+
const doc = (await res.json());
|
|
58
|
+
if (!doc.releases || typeof doc.releases !== "object")
|
|
59
|
+
throw new Error(`PyPI returned no release list for ${name}`);
|
|
60
|
+
const versions = [];
|
|
61
|
+
for (const [version, files] of Object.entries(doc.releases)) {
|
|
62
|
+
if (!parsePep440(version))
|
|
63
|
+
continue;
|
|
64
|
+
const list = Array.isArray(files) ? files : [];
|
|
65
|
+
const times = list.map((f) => f.upload_time_iso_8601).filter((t) => typeof t === "string").sort();
|
|
66
|
+
const entry = { version, published: times[0] ?? null };
|
|
67
|
+
if (list.length > 0 && list.every((f) => f.yanked)) {
|
|
68
|
+
entry.yanked = true;
|
|
69
|
+
const reason = list.find((f) => f.yanked_reason)?.yanked_reason;
|
|
70
|
+
if (reason)
|
|
71
|
+
entry.deprecated = `yanked: ${reason}`;
|
|
72
|
+
}
|
|
73
|
+
const sdist = pickSdist(list);
|
|
74
|
+
if (sdist)
|
|
75
|
+
entry.tarball = sdist;
|
|
76
|
+
versions.push(entry);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
name: doc.info?.name ?? name,
|
|
80
|
+
ecosystem: "PyPI",
|
|
81
|
+
versions,
|
|
82
|
+
repository: pypiRepository(doc.info),
|
|
83
|
+
changelogLinks: pypiChangelogLinks(doc.info),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Network sources for `upgrade_impact`: registry version index, the target
|
|
3
3
|
* version's changelog (read from its registry archive) and OSV advisories.
|
|
4
4
|
*
|
|
5
|
-
* Privacy rule (Decision 1, option A): the only hosts contacted
|
|
6
|
-
* package's own registry (registry.npmjs.org; crates.io +
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Privacy rule (Decision 1, option A): by default the only hosts contacted
|
|
6
|
+
* are the package's own registry (registry.npmjs.org; crates.io +
|
|
7
|
+
* static.crates.io; pypi.org + files.pythonhosted.org; proxy.golang.org) and
|
|
8
|
+
* api.osv.dev. A changelog is read from GitHub only when the user opts in
|
|
9
|
+
* (Decision 1, option B: FOURDA_GITHUB_CHANGELOG, github-releases.ts) and the
|
|
10
|
+
* archive has none for the range; otherwise the report hands the agent a
|
|
11
|
+
* `release_notes_url` built from registry metadata and says how to enable the
|
|
12
|
+
* fallback. Every request goes through `UpgradeNet`, which records the host
|
|
13
|
+
* so `_meta.sources` is a fact, not a claim, and which tests replace with a
|
|
14
|
+
* fake fetch.
|
|
13
15
|
*
|
|
14
16
|
* Request budget per call: index (1) + archive (1) + OSV for from/to (2) = 4,
|
|
15
17
|
* each cached (index 6 h, parsed changelog 7 d, OSV 1 h). The npm packument
|
|
@@ -17,8 +19,17 @@
|
|
|
17
19
|
* is cached — never the raw document.
|
|
18
20
|
*/
|
|
19
21
|
import { type FetchFn } from "./package-archive.js";
|
|
20
|
-
import { type ChangelogSection } from "./changelog.js";
|
|
21
|
-
export type UpgradeEcosystem = "npm" | "crates.io";
|
|
22
|
+
import { type ChangelogSection, type VersionOrder } from "./changelog.js";
|
|
23
|
+
export type UpgradeEcosystem = "npm" | "crates.io" | "PyPI" | "Go";
|
|
24
|
+
export declare const UPGRADE_ECOSYSTEMS: readonly UpgradeEcosystem[];
|
|
25
|
+
/** Version order for an ecosystem: semver precedence (npm, crates.io, Go without its `v`), PEP 440 (PyPI). Null = unreadable. */
|
|
26
|
+
export declare function versionOrder(ecosystem: UpgradeEcosystem): VersionOrder;
|
|
27
|
+
/** Readable as a version in this ecosystem. */
|
|
28
|
+
export declare function isReadableVersion(version: string, ecosystem: UpgradeEcosystem): boolean;
|
|
29
|
+
/** A prerelease in this ecosystem's own terms (PEP 440 `a`/`b`/`rc`/`.dev`; semver `-x`; a Go pseudo-version). */
|
|
30
|
+
export declare function isPrerelease(version: string, ecosystem: UpgradeEcosystem): boolean;
|
|
31
|
+
/** Package-name identity: crates.io folds `-`/`_`, PyPI normalises per PEP 503, npm and Go are exact. */
|
|
32
|
+
export declare function sameUpgradeName(a: string, b: string, ecosystem: string): boolean;
|
|
22
33
|
/** crates.io refuses API requests without a descriptive User-Agent. */
|
|
23
34
|
export declare const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
|
|
24
35
|
export interface PublishedVersion {
|
|
@@ -26,7 +37,7 @@ export interface PublishedVersion {
|
|
|
26
37
|
published: string | null;
|
|
27
38
|
deprecated?: string;
|
|
28
39
|
yanked?: boolean;
|
|
29
|
-
/** Archive URL (npm `dist.tarball
|
|
40
|
+
/** Archive URL (npm `dist.tarball`, PyPI sdist); crates and Go archives are derived from name + version. */
|
|
30
41
|
tarball?: string;
|
|
31
42
|
}
|
|
32
43
|
export interface RegistryIndex {
|
|
@@ -35,12 +46,18 @@ export interface RegistryIndex {
|
|
|
35
46
|
ecosystem: UpgradeEcosystem;
|
|
36
47
|
versions: PublishedVersion[];
|
|
37
48
|
repository: string | null;
|
|
49
|
+
/** Links the project itself labels as its changelog (PyPI `project_urls`). */
|
|
50
|
+
changelogLinks?: string[];
|
|
38
51
|
}
|
|
39
52
|
export interface ChangelogResult {
|
|
40
53
|
found: boolean;
|
|
41
54
|
file?: string;
|
|
42
55
|
sections?: ChangelogSection[];
|
|
43
56
|
reason?: string;
|
|
57
|
+
/** Where the sections came from: the registry archive (default), or the opt-in GitHub fallback. */
|
|
58
|
+
source?: "archive" | "github-releases" | "github-file";
|
|
59
|
+
/** The page or API the GitHub fallback read. */
|
|
60
|
+
url?: string;
|
|
44
61
|
}
|
|
45
62
|
/** The cache surface used here; `LiveCache` satisfies it. */
|
|
46
63
|
export interface UpgradeCache {
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
* Network sources for `upgrade_impact`: registry version index, the target
|
|
4
4
|
* version's changelog (read from its registry archive) and OSV advisories.
|
|
5
5
|
*
|
|
6
|
-
* Privacy rule (Decision 1, option A): the only hosts contacted
|
|
7
|
-
* package's own registry (registry.npmjs.org; crates.io +
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* Privacy rule (Decision 1, option A): by default the only hosts contacted
|
|
7
|
+
* are the package's own registry (registry.npmjs.org; crates.io +
|
|
8
|
+
* static.crates.io; pypi.org + files.pythonhosted.org; proxy.golang.org) and
|
|
9
|
+
* api.osv.dev. A changelog is read from GitHub only when the user opts in
|
|
10
|
+
* (Decision 1, option B: FOURDA_GITHUB_CHANGELOG, github-releases.ts) and the
|
|
11
|
+
* archive has none for the range; otherwise the report hands the agent a
|
|
12
|
+
* `release_notes_url` built from registry metadata and says how to enable the
|
|
13
|
+
* fallback. Every request goes through `UpgradeNet`, which records the host
|
|
14
|
+
* so `_meta.sources` is a fact, not a claim, and which tests replace with a
|
|
15
|
+
* fake fetch.
|
|
14
16
|
*
|
|
15
17
|
* Request budget per call: index (1) + archive (1) + OSV for from/to (2) = 4,
|
|
16
18
|
* each cached (index 6 h, parsed changelog 7 d, OSV 1 h). The npm packument
|
|
@@ -18,8 +20,40 @@
|
|
|
18
20
|
* is cached — never the raw document.
|
|
19
21
|
*/
|
|
20
22
|
import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
|
|
21
|
-
import { ArchiveError, defaultFetch,
|
|
23
|
+
import { ArchiveError, defaultFetch, fetchArchiveBytes, readRootTextFiles } from "./package-archive.js";
|
|
22
24
|
import { CHANGELOG_PARSER_VERSION, findChangelogFile, isChangelogName, parseChangelog } from "./changelog.js";
|
|
25
|
+
import { compareVersions, isComparable } from "./version-compare.js";
|
|
26
|
+
import { isPrereleaseVersion } from "./osv-ranges.js";
|
|
27
|
+
import { looksLikeZip, readZipRootTextFiles } from "./zip-archive.js";
|
|
28
|
+
import { loadPypiIndex, normalizePypiName, PYPI_NAME } from "./upgrade-pypi.js";
|
|
29
|
+
import { GO_MODULE, goZipUrl, loadGoIndex } from "./upgrade-go.js";
|
|
30
|
+
export const UPGRADE_ECOSYSTEMS = ["npm", "crates.io", "PyPI", "Go"];
|
|
31
|
+
/** Version order for an ecosystem: semver precedence (npm, crates.io, Go without its `v`), PEP 440 (PyPI). Null = unreadable. */
|
|
32
|
+
export function versionOrder(ecosystem) {
|
|
33
|
+
if (ecosystem === "npm" || ecosystem === "crates.io")
|
|
34
|
+
return compareVersionPrecedence;
|
|
35
|
+
return (a, b) => (isComparable(a, ecosystem) && isComparable(b, ecosystem) ? compareVersions(a, b, ecosystem) : null);
|
|
36
|
+
}
|
|
37
|
+
/** Readable as a version in this ecosystem. */
|
|
38
|
+
export function isReadableVersion(version, ecosystem) {
|
|
39
|
+
if (ecosystem === "npm" || ecosystem === "crates.io")
|
|
40
|
+
return parseSemverPrecedence(version) !== null;
|
|
41
|
+
return version !== "0" && isComparable(version, ecosystem);
|
|
42
|
+
}
|
|
43
|
+
/** A prerelease in this ecosystem's own terms (PEP 440 `a`/`b`/`rc`/`.dev`; semver `-x`; a Go pseudo-version). */
|
|
44
|
+
export function isPrerelease(version, ecosystem) {
|
|
45
|
+
if (ecosystem === "npm" || ecosystem === "crates.io")
|
|
46
|
+
return (parseSemverPrecedence(version)?.prerelease.length ?? 0) > 0;
|
|
47
|
+
return isPrereleaseVersion(version, ecosystem);
|
|
48
|
+
}
|
|
49
|
+
/** Package-name identity: crates.io folds `-`/`_`, PyPI normalises per PEP 503, npm and Go are exact. */
|
|
50
|
+
export function sameUpgradeName(a, b, ecosystem) {
|
|
51
|
+
if (ecosystem === "crates.io")
|
|
52
|
+
return a.replace(/-/g, "_") === b.replace(/-/g, "_");
|
|
53
|
+
if (ecosystem === "PyPI")
|
|
54
|
+
return normalizePypiName(a) === normalizePypiName(b);
|
|
55
|
+
return a === b;
|
|
56
|
+
}
|
|
23
57
|
/** crates.io refuses API requests without a descriptive User-Agent. */
|
|
24
58
|
export const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
|
|
25
59
|
const INDEX_TTL = 6 * 3600;
|
|
@@ -42,7 +76,16 @@ export function createUpgradeNet(cache, fetchFn = defaultFetch) {
|
|
|
42
76
|
const NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
|
|
43
77
|
const CRATE_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
44
78
|
export function isValidPackageName(name, ecosystem) {
|
|
45
|
-
|
|
79
|
+
switch (ecosystem) {
|
|
80
|
+
case "npm":
|
|
81
|
+
return NPM_NAME.test(name) && name.length <= 214;
|
|
82
|
+
case "crates.io":
|
|
83
|
+
return CRATE_NAME.test(name);
|
|
84
|
+
case "PyPI":
|
|
85
|
+
return PYPI_NAME.test(name);
|
|
86
|
+
case "Go":
|
|
87
|
+
return GO_MODULE.test(name) && name.length <= 300 && !name.split("/").some((p) => p === "." || p === "..");
|
|
88
|
+
}
|
|
46
89
|
}
|
|
47
90
|
async function cached(net, key, ttl, load) {
|
|
48
91
|
const hit = net.cache?.get(key);
|
|
@@ -99,21 +142,36 @@ async function loadCratesIndex(net, name) {
|
|
|
99
142
|
}
|
|
100
143
|
/** Registry index with versions in ascending precedence order (unreadable versions dropped). Null = unknown package. */
|
|
101
144
|
export async function getRegistryIndex(net, ecosystem, name) {
|
|
102
|
-
const
|
|
103
|
-
const
|
|
145
|
+
const keyName = ecosystem === "crates.io" ? name.replace(/-/g, "_") : ecosystem === "PyPI" ? normalizePypiName(name) : name;
|
|
146
|
+
const key = `upgrade-impact:index:${ecosystem}:${keyName}`;
|
|
147
|
+
const index = await cached(net, key, INDEX_TTL, () => {
|
|
148
|
+
switch (ecosystem) {
|
|
149
|
+
case "npm":
|
|
150
|
+
return loadNpmIndex(net, name);
|
|
151
|
+
case "crates.io":
|
|
152
|
+
return loadCratesIndex(net, name);
|
|
153
|
+
case "PyPI":
|
|
154
|
+
return loadPypiIndex(net, name);
|
|
155
|
+
case "Go":
|
|
156
|
+
return loadGoIndex(net, name);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
104
159
|
if (!index)
|
|
105
160
|
return null;
|
|
161
|
+
const order = versionOrder(ecosystem);
|
|
106
162
|
const versions = index.versions
|
|
107
|
-
.filter((v) =>
|
|
108
|
-
.sort((a, b) =>
|
|
163
|
+
.filter((v) => isReadableVersion(v.version, ecosystem))
|
|
164
|
+
.sort((a, b) => order(a.version, b.version) ?? 0);
|
|
109
165
|
return { ...index, versions };
|
|
110
166
|
}
|
|
111
|
-
const ARCHIVE_HOSTS = new Set(["registry.npmjs.org", "static.crates.io"]);
|
|
167
|
+
const ARCHIVE_HOSTS = new Set(["registry.npmjs.org", "static.crates.io", "files.pythonhosted.org", "proxy.golang.org"]);
|
|
112
168
|
/** The registry archive URL for one version, or null when it is not on the package's own registry. */
|
|
113
169
|
export function archiveUrl(index, version) {
|
|
114
170
|
const url = index.ecosystem === "crates.io"
|
|
115
171
|
? `https://static.crates.io/crates/${index.name}/${index.name}-${version.version}.crate`
|
|
116
|
-
:
|
|
172
|
+
: index.ecosystem === "Go"
|
|
173
|
+
? goZipUrl(index.name, version.version)
|
|
174
|
+
: version.tarball;
|
|
117
175
|
if (!url)
|
|
118
176
|
return null;
|
|
119
177
|
try {
|
|
@@ -132,7 +190,12 @@ export function changelogCacheKey(ecosystem, name, version) {
|
|
|
132
190
|
export async function getChangelog(net, index, version) {
|
|
133
191
|
const url = archiveUrl(index, version);
|
|
134
192
|
if (!url) {
|
|
135
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
found: false,
|
|
195
|
+
reason: index.ecosystem === "PyPI" && !version.tarball
|
|
196
|
+
? `${version.version} has no source distribution on PyPI (wheels only), so there is no archive changelog to read`
|
|
197
|
+
: "the version's archive is not hosted on the package's own registry",
|
|
198
|
+
};
|
|
136
199
|
}
|
|
137
200
|
// The parser version is in the key: a parser fix must not wait out the 7-day TTL.
|
|
138
201
|
const key = changelogCacheKey(index.ecosystem, index.name, version.version);
|
|
@@ -142,7 +205,11 @@ export async function getChangelog(net, index, version) {
|
|
|
142
205
|
const headers = index.ecosystem === "crates.io" ? { "User-Agent": CRATES_USER_AGENT } : {};
|
|
143
206
|
let files;
|
|
144
207
|
try {
|
|
145
|
-
|
|
208
|
+
const bytes = await fetchArchiveBytes(url, net.fetch, headers);
|
|
209
|
+
// A Go module zip holds its files under `<module>@<version>/`; a zip sdist and every tar one directory deep.
|
|
210
|
+
files = looksLikeZip(bytes)
|
|
211
|
+
? readZipRootTextFiles(bytes, isChangelogName, index.ecosystem === "Go" ? `${index.name}@${version.version}/` : null)
|
|
212
|
+
: readRootTextFiles(bytes, isChangelogName);
|
|
146
213
|
}
|
|
147
214
|
catch (err) {
|
|
148
215
|
const result = { found: false, reason: `could not read the ${version.version} archive: ${err.message}` };
|
|
@@ -158,7 +225,7 @@ export async function getChangelog(net, index, version) {
|
|
|
158
225
|
? { found: false, reason: `the ${version.version} archive ships no changelog file` }
|
|
159
226
|
: sections.length === 0
|
|
160
227
|
? { found: false, file, reason: `${file} has no version headings this parser recognises` }
|
|
161
|
-
: { found: true, file, sections };
|
|
228
|
+
: { found: true, file, sections, source: "archive" };
|
|
162
229
|
net.cache?.set(key, result, CACHE_SOURCE, CHANGELOG_TTL);
|
|
163
230
|
return result;
|
|
164
231
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zip archive reader for `upgrade_impact`, Node built-ins only.
|
|
3
|
+
*
|
|
4
|
+
* Go module zips (proxy.golang.org `/@v/<version>.zip`) and the occasional
|
|
5
|
+
* PyPI `.zip` sdist are zip files, not gzip tars. The job is the same as
|
|
6
|
+
* package-archive.ts's: pull a few root-level text files (the changelog) out
|
|
7
|
+
* of a third-party archive without trusting it. Only the central directory
|
|
8
|
+
* is read, so a file's declared sizes come from one place; stored (0) and
|
|
9
|
+
* deflate (8) entries are read, everything else (zip64, encryption, other
|
|
10
|
+
* methods) is skipped rather than guessed at. Each inflate is capped at
|
|
11
|
+
* MAX_FILE_BYTES, so a zip bomb stops at the cap.
|
|
12
|
+
*/
|
|
13
|
+
/** True when the buffer starts like a zip archive (a local file header). */
|
|
14
|
+
export declare function looksLikeZip(buf: Buffer): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Text files directly under `root` (a path prefix such as
|
|
17
|
+
* `github.com/foo/bar@v1.2.3/`; "" for every top-level file of a one-directory
|
|
18
|
+
* archive) whose basename `accept` approves. When `root` is null, files one
|
|
19
|
+
* directory deep are read, as the tar reader does. Oversized, binary and
|
|
20
|
+
* unreadable entries are skipped.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readZipRootTextFiles(buf: Buffer, accept: (basename: string) => boolean, root?: string | null): Map<string, string>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Zip archive reader for `upgrade_impact`, Node built-ins only.
|
|
4
|
+
*
|
|
5
|
+
* Go module zips (proxy.golang.org `/@v/<version>.zip`) and the occasional
|
|
6
|
+
* PyPI `.zip` sdist are zip files, not gzip tars. The job is the same as
|
|
7
|
+
* package-archive.ts's: pull a few root-level text files (the changelog) out
|
|
8
|
+
* of a third-party archive without trusting it. Only the central directory
|
|
9
|
+
* is read, so a file's declared sizes come from one place; stored (0) and
|
|
10
|
+
* deflate (8) entries are read, everything else (zip64, encryption, other
|
|
11
|
+
* methods) is skipped rather than guessed at. Each inflate is capped at
|
|
12
|
+
* MAX_FILE_BYTES, so a zip bomb stops at the cap.
|
|
13
|
+
*/
|
|
14
|
+
import { inflateRawSync } from "node:zlib";
|
|
15
|
+
import { ArchiveError, MAX_COMPRESSED_BYTES, MAX_FILE_BYTES } from "./package-archive.js";
|
|
16
|
+
const EOCD_SIG = 0x06054b50;
|
|
17
|
+
const CDH_SIG = 0x02014b50;
|
|
18
|
+
const LFH_SIG = 0x04034b50;
|
|
19
|
+
/** The end-of-central-directory record is 22 bytes plus a comment of at most 65,535. */
|
|
20
|
+
const EOCD_SEARCH = 22 + 0xffff;
|
|
21
|
+
const MAX_ENTRIES = 200_000;
|
|
22
|
+
/** True when the buffer starts like a zip archive (a local file header). */
|
|
23
|
+
export function looksLikeZip(buf) {
|
|
24
|
+
return buf.length >= 4 && buf.readUInt32LE(0) === LFH_SIG;
|
|
25
|
+
}
|
|
26
|
+
function findEocd(buf) {
|
|
27
|
+
const stop = Math.max(0, buf.length - EOCD_SEARCH);
|
|
28
|
+
for (let i = buf.length - 22; i >= stop; i--) {
|
|
29
|
+
if (buf.readUInt32LE(i) === EOCD_SIG)
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
throw new ArchiveError("not a zip archive (no end-of-central-directory record)");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Text files directly under `root` (a path prefix such as
|
|
36
|
+
* `github.com/foo/bar@v1.2.3/`; "" for every top-level file of a one-directory
|
|
37
|
+
* archive) whose basename `accept` approves. When `root` is null, files one
|
|
38
|
+
* directory deep are read, as the tar reader does. Oversized, binary and
|
|
39
|
+
* unreadable entries are skipped.
|
|
40
|
+
*/
|
|
41
|
+
export function readZipRootTextFiles(buf, accept, root = null) {
|
|
42
|
+
if (buf.length > MAX_COMPRESSED_BYTES) {
|
|
43
|
+
throw new ArchiveError(`archive is ${buf.length} bytes compressed (cap ${MAX_COMPRESSED_BYTES})`);
|
|
44
|
+
}
|
|
45
|
+
if (buf.length < 22)
|
|
46
|
+
throw new ArchiveError("not a zip archive (too short)");
|
|
47
|
+
const eocd = findEocd(buf);
|
|
48
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
49
|
+
const cdSize = buf.readUInt32LE(eocd + 12);
|
|
50
|
+
const cdOffset = buf.readUInt32LE(eocd + 16);
|
|
51
|
+
if (cdOffset === 0xffffffff || count === 0xffff)
|
|
52
|
+
throw new ArchiveError("zip64 archives are not read");
|
|
53
|
+
if (cdOffset + cdSize > buf.length || count > MAX_ENTRIES)
|
|
54
|
+
throw new ArchiveError("corrupt zip central directory");
|
|
55
|
+
const files = new Map();
|
|
56
|
+
let p = cdOffset;
|
|
57
|
+
for (let n = 0; n < count; n++) {
|
|
58
|
+
if (p + 46 > buf.length || buf.readUInt32LE(p) !== CDH_SIG)
|
|
59
|
+
throw new ArchiveError("corrupt zip central directory entry");
|
|
60
|
+
const flags = buf.readUInt16LE(p + 8);
|
|
61
|
+
const method = buf.readUInt16LE(p + 10);
|
|
62
|
+
const compressed = buf.readUInt32LE(p + 20);
|
|
63
|
+
const size = buf.readUInt32LE(p + 24);
|
|
64
|
+
const nameLen = buf.readUInt16LE(p + 28);
|
|
65
|
+
const extraLen = buf.readUInt16LE(p + 30);
|
|
66
|
+
const commentLen = buf.readUInt16LE(p + 32);
|
|
67
|
+
const local = buf.readUInt32LE(p + 42);
|
|
68
|
+
const name = buf.subarray(p + 46, p + 46 + nameLen).toString("utf8");
|
|
69
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
70
|
+
if (name.endsWith("/") || !isRootFile(name, root) || !accept(name.split("/").pop() ?? ""))
|
|
71
|
+
continue;
|
|
72
|
+
if (flags & 0x1 || (method !== 0 && method !== 8) || size > MAX_FILE_BYTES)
|
|
73
|
+
continue;
|
|
74
|
+
if (local + 30 > buf.length || buf.readUInt32LE(local) !== LFH_SIG)
|
|
75
|
+
continue;
|
|
76
|
+
const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
|
|
77
|
+
if (start + compressed > buf.length)
|
|
78
|
+
continue;
|
|
79
|
+
const raw = buf.subarray(start, start + compressed);
|
|
80
|
+
let data;
|
|
81
|
+
try {
|
|
82
|
+
data = method === 0 ? Buffer.from(raw) : inflateRawSync(raw, { maxOutputLength: MAX_FILE_BYTES });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (data.includes(0))
|
|
88
|
+
continue;
|
|
89
|
+
files.set(name, data.toString("utf8"));
|
|
90
|
+
}
|
|
91
|
+
return files;
|
|
92
|
+
}
|
|
93
|
+
function isRootFile(name, root) {
|
|
94
|
+
if (root !== null)
|
|
95
|
+
return name.startsWith(root) && !name.slice(root.length).includes("/") && name.length > root.length;
|
|
96
|
+
const parts = name.split("/").filter(Boolean);
|
|
97
|
+
return parts.length > 0 && parts.length <= 2;
|
|
98
|
+
}
|
package/dist/schema-registry.js
CHANGED
|
@@ -31,7 +31,7 @@ const WRITE_LOCAL = { readOnlyHint: false, openWorldHint: false, destructiveHint
|
|
|
31
31
|
export const TOOL_REGISTRY = {
|
|
32
32
|
// --- Dependency Security (standalone) ---
|
|
33
33
|
vulnerability_scan: {
|
|
34
|
-
summary: "Scan this project's lockfiles (npm/pnpm/yarn/bun, Cargo, Python, Go) for
|
|
34
|
+
summary: "Scan this project's lockfiles (npm/pnpm/yarn/bun, Cargo, Python, Go) for OSV.dev vulnerabilities in installed versions, transitives included, with fix versions and where each is pinned; `package` for one dep. Call when asked about security or CVEs. To vet a version first: dependency_check.",
|
|
35
35
|
definition: vulnerabilityScanTool,
|
|
36
36
|
category: "security",
|
|
37
37
|
tags: ["security", "vulnerabilities", "cve", "dependencies", "osv"],
|
|
@@ -55,7 +55,7 @@ export const TOOL_REGISTRY = {
|
|
|
55
55
|
annotations: { title: "Upgrade planner", ...READ_NETWORK },
|
|
56
56
|
},
|
|
57
57
|
upgrade_impact: {
|
|
58
|
-
summary: "What changes between the installed version of ONE dependency and a target
|
|
58
|
+
summary: "What changes between the installed version of ONE dependency and a target: changelog per release, breaking changes, deprecations, security fixes, and this project's files that import it. Call before upgrading or bumping a dependency.",
|
|
59
59
|
definition: upgradeImpactTool,
|
|
60
60
|
category: "security",
|
|
61
61
|
tags: ["upgrade", "changelog", "breaking-changes", "migration", "release-notes"],
|
|
@@ -63,7 +63,7 @@ export const TOOL_REGISTRY = {
|
|
|
63
63
|
annotations: { title: "Upgrade impact", ...READ_NETWORK },
|
|
64
64
|
},
|
|
65
65
|
dependency_check: {
|
|
66
|
-
summary: "
|
|
66
|
+
summary: "Is a dependency version safe to write? Verdict on an exact version, or safe releases in a constraint (\"7.x\", \"~=3.1\"); npm, crates.io, PyPI, Go. Call BEFORE writing any version to a manifest: add, pin, upgrade, bump, downgrade.",
|
|
67
67
|
definition: dependencyCheckTool,
|
|
68
68
|
category: "security",
|
|
69
69
|
tags: ["dependencies", "supply-chain", "pre-install", "upgrade", "versions"],
|