@4da/mcp-server 5.0.4 → 6.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/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +233 -37
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Python and Go lockfile parsers (split out of lockfile-parsers.ts).
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-10-02 against osv-scanner and pip-audit, every miss below was
|
|
6
|
+
* a parsing loss before OSV was ever asked:
|
|
7
|
+
* - requirements.txt: `urllib3[secure]==1.25.0` and dotted names were skipped,
|
|
8
|
+
* and an inline comment rode along as part of the version — OSV was asked
|
|
9
|
+
* about `"8.0.0 # imaging"` and answered with 63 advisories for the real 45.
|
|
10
|
+
* - poetry.lock: the reader required `name` and `version` on consecutive
|
|
11
|
+
* lines. Poetry before 1.0 wrote its keys alphabetically (`category`,
|
|
12
|
+
* `description`, `name`, ..., `version`), so a whole lockfile read as empty.
|
|
13
|
+
* - go.sum: `(v[\d.]+)` cut `v0.0.0-20200622213623-75b288015ac9` to `v0.0.0`
|
|
14
|
+
* and dropped `+incompatible`; `v0.0.0` then sorted AFTER the real
|
|
15
|
+
* pseudo-version, so older advisories were missed and later ones invented.
|
|
16
|
+
* go.sum also lists versions the build never selected.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { emptySource, found, InstanceSet } from "./lockfile-types.js";
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Python: poetry.lock > uv.lock > Pipfile.lock > requirements.txt
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/** PEP 503 normalised name: lowercase, runs of `-`, `_`, `.` become one `-`. */
|
|
25
|
+
export function normalizePythonName(name) {
|
|
26
|
+
return name.trim().toLowerCase().replace(/[-_.]+/g, "-");
|
|
27
|
+
}
|
|
28
|
+
export function resolvePython(cwd) {
|
|
29
|
+
// poetry.lock and uv.lock are TOML arrays of [[package]] tables.
|
|
30
|
+
for (const file of ["poetry.lock", "uv.lock"]) {
|
|
31
|
+
const lockPath = path.join(cwd, file);
|
|
32
|
+
if (!fs.existsSync(lockPath))
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
const read = readTomlPackageLock(fs.readFileSync(lockPath, "utf-8"));
|
|
36
|
+
if (read.instances.size > 0)
|
|
37
|
+
return found(read.versions, read.instances, lockPath, "lockfile");
|
|
38
|
+
}
|
|
39
|
+
catch { /* fall through */ }
|
|
40
|
+
}
|
|
41
|
+
const pipfileLock = path.join(cwd, "Pipfile.lock");
|
|
42
|
+
if (fs.existsSync(pipfileLock)) {
|
|
43
|
+
try {
|
|
44
|
+
const lock = JSON.parse(fs.readFileSync(pipfileLock, "utf-8"));
|
|
45
|
+
const versions = new Map();
|
|
46
|
+
const instances = new InstanceSet();
|
|
47
|
+
for (const [section, dev] of [["default", false], ["develop", true]]) {
|
|
48
|
+
for (const [name, info] of Object.entries(lock[section] ?? {})) {
|
|
49
|
+
const version = info.version?.replace(/^===?/, "");
|
|
50
|
+
if (!version)
|
|
51
|
+
continue;
|
|
52
|
+
const normalized = normalizePythonName(name);
|
|
53
|
+
instances.add(normalized, version, dev);
|
|
54
|
+
if (!versions.has(normalized))
|
|
55
|
+
versions.set(normalized, version);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (instances.size > 0)
|
|
59
|
+
return found(versions, instances, pipfileLock, "lockfile");
|
|
60
|
+
}
|
|
61
|
+
catch { /* fall through */ }
|
|
62
|
+
}
|
|
63
|
+
const reqPath = path.join(cwd, "requirements.txt");
|
|
64
|
+
if (fs.existsSync(reqPath)) {
|
|
65
|
+
const versions = new Map();
|
|
66
|
+
const instances = new InstanceSet();
|
|
67
|
+
try {
|
|
68
|
+
for (const { name, version } of parseRequirements(fs.readFileSync(reqPath, "utf-8"))) {
|
|
69
|
+
instances.add(name, version);
|
|
70
|
+
if (!versions.has(name))
|
|
71
|
+
versions.set(name, version);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { /* skip */ }
|
|
75
|
+
return found(versions, instances, reqPath, "manifest");
|
|
76
|
+
}
|
|
77
|
+
return emptySource();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* [[package]] tables from poetry.lock (any version) or uv.lock. A pre-1.2
|
|
81
|
+
* poetry.lock records `category = "dev"` for dev-only packages; newer files
|
|
82
|
+
* and uv.lock do not record scope here, so it stays unknown.
|
|
83
|
+
*/
|
|
84
|
+
export function readTomlPackageLock(content) {
|
|
85
|
+
const versions = new Map();
|
|
86
|
+
const instances = new InstanceSet();
|
|
87
|
+
for (const block of content.split(/^\[\[package\]\]\s*$/m).slice(1)) {
|
|
88
|
+
// Stop at the next table header (e.g. [package.dependencies], [metadata]).
|
|
89
|
+
const body = block.split(/^\[/m)[0];
|
|
90
|
+
const field = (key) => new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m").exec(body)?.[1] ?? null;
|
|
91
|
+
const name = field("name");
|
|
92
|
+
const version = field("version");
|
|
93
|
+
if (!name || !version)
|
|
94
|
+
continue;
|
|
95
|
+
// uv.lock lists the project itself (source = { editable/virtual = "." }); it is not a release.
|
|
96
|
+
if (/^source\s*=\s*\{\s*(?:editable|virtual)\s*=/m.test(body))
|
|
97
|
+
continue;
|
|
98
|
+
const category = field("category");
|
|
99
|
+
const normalized = normalizePythonName(name);
|
|
100
|
+
instances.add(normalized, version, category === null ? undefined : category === "dev");
|
|
101
|
+
if (!versions.has(normalized))
|
|
102
|
+
versions.set(normalized, version);
|
|
103
|
+
}
|
|
104
|
+
return { versions, instances };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Exact pins from a requirements file: `name==1.2`, `name===1.2`,
|
|
108
|
+
* `name[extra1,extra2]==1.2`, with environment markers and inline comments.
|
|
109
|
+
* Ranges (`>=`, `~=`) do not name an installed version and are skipped, as are
|
|
110
|
+
* options (`-r`, `-e`, `--hash`) and URL requirements.
|
|
111
|
+
*/
|
|
112
|
+
export function parseRequirements(content) {
|
|
113
|
+
const out = [];
|
|
114
|
+
// Backslash continuations join physical lines into one requirement.
|
|
115
|
+
const logical = content.replace(/\\\r?\n/g, " ").split(/\r?\n/);
|
|
116
|
+
for (const raw of logical) {
|
|
117
|
+
// A `#` starts a comment at line start or after whitespace (PEP 508 URLs may contain `#`).
|
|
118
|
+
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
119
|
+
if (!line || line.startsWith("-"))
|
|
120
|
+
continue;
|
|
121
|
+
// Per-requirement options (pip-compile writes `--hash=sha256:...` on
|
|
122
|
+
// continuation lines) follow the specifier; they are not part of it.
|
|
123
|
+
const spec = line.split(";")[0].replace(/\s--?[A-Za-z][\w-]*(?:[=\s]\S+)?/g, "").trim();
|
|
124
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*===?\s*([^\s,;]+)\s*$/.exec(spec);
|
|
125
|
+
if (!match)
|
|
126
|
+
continue;
|
|
127
|
+
out.push({ name: normalizePythonName(match[1]), version: match[2] });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Go: go.mod (the selected module set) completed from go.sum for go < 1.17
|
|
133
|
+
// =============================================================================
|
|
134
|
+
/** A full Go module version: release, prerelease, pseudo-version, `+incompatible`. */
|
|
135
|
+
const GO_VERSION = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
136
|
+
export function resolveGo(cwd) {
|
|
137
|
+
const goModPath = path.join(cwd, "go.mod");
|
|
138
|
+
if (!fs.existsSync(goModPath))
|
|
139
|
+
return emptySource();
|
|
140
|
+
let mod;
|
|
141
|
+
try {
|
|
142
|
+
mod = parseGoMod(fs.readFileSync(goModPath, "utf-8"));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return emptySource();
|
|
146
|
+
}
|
|
147
|
+
const versions = new Map();
|
|
148
|
+
const instances = new InstanceSet();
|
|
149
|
+
for (const { module, version } of mod.requires) {
|
|
150
|
+
versions.set(module, version);
|
|
151
|
+
instances.add(module, version);
|
|
152
|
+
}
|
|
153
|
+
// From go 1.17 the go.mod `require` list names every module the build needs
|
|
154
|
+
// (module-graph pruning), so it IS the selected set and go.sum (which keeps
|
|
155
|
+
// hashes for versions the build never selects) would only over-report.
|
|
156
|
+
// Before 1.17 go.mod lists direct requirements only; transitives come from
|
|
157
|
+
// go.sum, at the highest version whose module zip is hashed there — what
|
|
158
|
+
// minimal version selection picked.
|
|
159
|
+
if (!mod.goVersionAtLeast117) {
|
|
160
|
+
const goSumPath = path.join(cwd, "go.sum");
|
|
161
|
+
if (fs.existsSync(goSumPath)) {
|
|
162
|
+
try {
|
|
163
|
+
for (const [module, version] of selectedFromGoSum(fs.readFileSync(goSumPath, "utf-8"))) {
|
|
164
|
+
if (versions.has(module))
|
|
165
|
+
continue;
|
|
166
|
+
versions.set(module, version);
|
|
167
|
+
instances.add(module, version);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { /* skip */ }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return found(versions, instances, goModPath, "lockfile");
|
|
174
|
+
}
|
|
175
|
+
/** `require` lines (block and single form) with `replace` applied, and whether the go directive is >= 1.17. */
|
|
176
|
+
export function parseGoMod(content) {
|
|
177
|
+
const requires = new Map();
|
|
178
|
+
const replaces = new Map();
|
|
179
|
+
let block = null;
|
|
180
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
181
|
+
const indirect = /\/\/\s*indirect\b/.test(rawLine);
|
|
182
|
+
const line = rawLine.replace(/\/\/.*$/, "").trim();
|
|
183
|
+
if (!line)
|
|
184
|
+
continue;
|
|
185
|
+
if (block && line === ")") {
|
|
186
|
+
block = null;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const opener = /^(require|replace)\s*\($/.exec(line);
|
|
190
|
+
if (opener) {
|
|
191
|
+
block = opener[1];
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const single = /^(require|replace)\s+(.+)$/.exec(line);
|
|
195
|
+
const kind = single ? single[1] : block;
|
|
196
|
+
const rest = single ? single[2] : line;
|
|
197
|
+
if (kind === "require") {
|
|
198
|
+
const [module, version] = rest.split(/\s+/);
|
|
199
|
+
if (module && version && GO_VERSION.test(version))
|
|
200
|
+
requires.set(module, { module, version, indirect });
|
|
201
|
+
}
|
|
202
|
+
else if (kind === "replace") {
|
|
203
|
+
// `old [v] => new v` replaces with another module version; `=> ./path` is local code.
|
|
204
|
+
const [left, right] = rest.split("=>").map((s) => s.trim().split(/\s+/));
|
|
205
|
+
if (!left?.[0] || !right?.[0])
|
|
206
|
+
continue;
|
|
207
|
+
replaces.set(left[0], right[1] && GO_VERSION.test(right[1]) ? { module: right[0], version: right[1] } : null);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const result = [];
|
|
211
|
+
for (const req of requires.values()) {
|
|
212
|
+
if (!replaces.has(req.module)) {
|
|
213
|
+
result.push(req);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const target = replaces.get(req.module);
|
|
217
|
+
if (target)
|
|
218
|
+
result.push({ module: target.module, version: target.version, indirect: req.indirect });
|
|
219
|
+
}
|
|
220
|
+
const go = /^go\s+(\d+)\.(\d+)/m.exec(content);
|
|
221
|
+
const goVersionAtLeast117 = go ? Number(go[1]) > 1 || Number(go[2]) >= 17 : false;
|
|
222
|
+
return { requires: result, goVersionAtLeast117 };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Per module, the highest version go.sum records — minimal version selection
|
|
226
|
+
* picks the highest required version, and go.sum records every version the
|
|
227
|
+
* module graph required. A module listed only by its `/go.mod` hash is still
|
|
228
|
+
* in Go's build list (`go list -m all`): measured 2026-10-02 on gh cli v1.0
|
|
229
|
+
* (go 1.13), skipping those lines missed 14 advisories (grpc 1.21.0,
|
|
230
|
+
* jwt-go, gorilla/websocket, ...) the build list carries.
|
|
231
|
+
*/
|
|
232
|
+
export function selectedFromGoSum(content) {
|
|
233
|
+
const selected = new Map();
|
|
234
|
+
for (const line of content.split(/\r?\n/)) {
|
|
235
|
+
const [module, rawVersion] = line.trim().split(/\s+/);
|
|
236
|
+
const version = rawVersion?.replace(/\/go\.mod$/, "");
|
|
237
|
+
if (!module || !version || !GO_VERSION.test(version))
|
|
238
|
+
continue;
|
|
239
|
+
const current = selected.get(module);
|
|
240
|
+
if (!current || compareGoVersions(version, current) > 0)
|
|
241
|
+
selected.set(module, version);
|
|
242
|
+
}
|
|
243
|
+
return selected;
|
|
244
|
+
}
|
|
245
|
+
/** Semver precedence over Go versions; build metadata (`+incompatible`) is ignored. */
|
|
246
|
+
export function compareGoVersions(a, b) {
|
|
247
|
+
const parse = (v) => {
|
|
248
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);
|
|
249
|
+
return m ? { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split(".") : [] } : null;
|
|
250
|
+
};
|
|
251
|
+
const pa = parse(a);
|
|
252
|
+
const pb = parse(b);
|
|
253
|
+
if (!pa || !pb)
|
|
254
|
+
return 0;
|
|
255
|
+
for (let i = 0; i < 3; i++)
|
|
256
|
+
if (pa.nums[i] !== pb.nums[i])
|
|
257
|
+
return pa.nums[i] > pb.nums[i] ? 1 : -1;
|
|
258
|
+
if (pa.pre.length === 0 || pb.pre.length === 0)
|
|
259
|
+
return Math.sign(pb.pre.length - pa.pre.length);
|
|
260
|
+
for (let i = 0; i < Math.min(pa.pre.length, pb.pre.length); i++) {
|
|
261
|
+
const x = pa.pre[i];
|
|
262
|
+
const y = pb.pre[i];
|
|
263
|
+
if (x === y)
|
|
264
|
+
continue;
|
|
265
|
+
const xn = /^\d+$/.test(x);
|
|
266
|
+
const yn = /^\d+$/.test(y);
|
|
267
|
+
if (xn && yn)
|
|
268
|
+
return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
269
|
+
if (xn !== yn)
|
|
270
|
+
return xn ? -1 : 1;
|
|
271
|
+
return x > y ? 1 : -1;
|
|
272
|
+
}
|
|
273
|
+
return Math.sign(pa.pre.length - pb.pre.length);
|
|
274
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lockfile parsers, one per ecosystem, each reporting WHICH file it resolved
|
|
3
|
+
* from as well as the versions it read.
|
|
4
|
+
*
|
|
5
|
+
* Priority per ecosystem: lock file (exact) > manifest (range/specifier).
|
|
6
|
+
* Uses only Node.js built-ins — no external parsers. The source path is what
|
|
7
|
+
* lets the live layer notice when that file changes and re-resolve (measured
|
|
8
|
+
* 2026-09-10: a server started before a lockfile bump kept reporting hono
|
|
9
|
+
* 4.13.3 after the lockfile said 4.13.5). The pnpm and yarn readers live in
|
|
10
|
+
* js-lockfile-readers.ts; Python and Go in lockfile-parsers-pygo.ts.
|
|
11
|
+
*
|
|
12
|
+
* Every parser reports two things (2026-10-02):
|
|
13
|
+
* - `instances`: EVERY installed copy, name@version, with the lockfile's own
|
|
14
|
+
* dev flag when it records one. A lockfile routinely holds several versions
|
|
15
|
+
* of one package (nested npm copies, two semver-incompatible crates). The
|
|
16
|
+
* reader used to keep one version per name, last one wins: measured against
|
|
17
|
+
* osv-scanner on 12 projects, nodegoat lost 287 of 1,091 installed copies
|
|
18
|
+
* and 4DA's src-tauri lost 143 of 934 — including its own `rsa 0.9.10`
|
|
19
|
+
* (RUSTSEC-2023-0071), overwritten by `rsa 0.10.0-rc.18` — and the direct
|
|
20
|
+
* `minimist@1.2.0` was reported as its nested `0.0.8`.
|
|
21
|
+
* - `versions`: the ONE version per name a direct dependency resolves to (the
|
|
22
|
+
* top-level npm copy, the crate version a workspace member depends on),
|
|
23
|
+
* which dependency health and upgrade planning read.
|
|
24
|
+
*/
|
|
25
|
+
import { InstanceSet, type VersionSource } from "./lockfile-types.js";
|
|
26
|
+
import type { OsvEcosystem } from "./types.js";
|
|
27
|
+
export type { PackageInstance, VersionSource } from "./lockfile-types.js";
|
|
28
|
+
/** Every lock file the resolver may read for this ecosystem, present or not, in priority order. */
|
|
29
|
+
export declare function lockfileCandidates(cwd: string, ecosystem: OsvEcosystem): string[];
|
|
30
|
+
/** The manifest the resolver falls back to for this ecosystem, or null when it has none. */
|
|
31
|
+
export declare function manifestCandidate(cwd: string, ecosystem: OsvEcosystem): string | null;
|
|
32
|
+
export declare function resolveVersionSource(cwd: string, ecosystem: OsvEcosystem): VersionSource;
|
|
33
|
+
/** The package name a package-lock `packages` key installs: the segment after its LAST `node_modules/`. */
|
|
34
|
+
export declare function packageNameFromNodeModulesPath(lockPath: string): string;
|
|
35
|
+
interface LockEntry {
|
|
36
|
+
version?: string;
|
|
37
|
+
dev?: boolean;
|
|
38
|
+
link?: boolean;
|
|
39
|
+
dependencies?: Record<string, LockEntry>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* package-lock.json / npm-shrinkwrap.json, lockfile v1, v2 and v3.
|
|
43
|
+
*
|
|
44
|
+
* v2/v3 `packages` keys are install paths: `node_modules/a` is the hoisted
|
|
45
|
+
* copy a direct dependency of the root resolves to, `node_modules/a/node_modules/b`
|
|
46
|
+
* a nested one. npm writes `dev: true` on every entry only reachable from
|
|
47
|
+
* devDependencies (`devOptional` = also reachable as an optional runtime dep,
|
|
48
|
+
* so runtime), which makes the dev scope of a TRANSITIVE known. v1 nests the
|
|
49
|
+
* same information under `dependencies`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readPackageLock(lock: {
|
|
52
|
+
packages?: Record<string, LockEntry>;
|
|
53
|
+
dependencies?: Record<string, LockEntry>;
|
|
54
|
+
}): {
|
|
55
|
+
versions: Map<string, string>;
|
|
56
|
+
instances: InstanceSet;
|
|
57
|
+
};
|
|
58
|
+
interface CargoPackage {
|
|
59
|
+
name: string;
|
|
60
|
+
version: string;
|
|
61
|
+
source: string | null;
|
|
62
|
+
dependencies: string[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every `[[package]]` table in a Cargo.lock, read field by field. The old
|
|
66
|
+
* reader required `name` and `version` on consecutive lines; that holds for
|
|
67
|
+
* cargo's own writer, but the table is TOML and nothing else guarantees it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseCargoLockPackages(content: string): CargoPackage[];
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Lockfile parsers, one per ecosystem, each reporting WHICH file it resolved
|
|
4
|
+
* from as well as the versions it read.
|
|
5
|
+
*
|
|
6
|
+
* Priority per ecosystem: lock file (exact) > manifest (range/specifier).
|
|
7
|
+
* Uses only Node.js built-ins — no external parsers. The source path is what
|
|
8
|
+
* lets the live layer notice when that file changes and re-resolve (measured
|
|
9
|
+
* 2026-09-10: a server started before a lockfile bump kept reporting hono
|
|
10
|
+
* 4.13.3 after the lockfile said 4.13.5). The pnpm and yarn readers live in
|
|
11
|
+
* js-lockfile-readers.ts; Python and Go in lockfile-parsers-pygo.ts.
|
|
12
|
+
*
|
|
13
|
+
* Every parser reports two things (2026-10-02):
|
|
14
|
+
* - `instances`: EVERY installed copy, name@version, with the lockfile's own
|
|
15
|
+
* dev flag when it records one. A lockfile routinely holds several versions
|
|
16
|
+
* of one package (nested npm copies, two semver-incompatible crates). The
|
|
17
|
+
* reader used to keep one version per name, last one wins: measured against
|
|
18
|
+
* osv-scanner on 12 projects, nodegoat lost 287 of 1,091 installed copies
|
|
19
|
+
* and 4DA's src-tauri lost 143 of 934 — including its own `rsa 0.9.10`
|
|
20
|
+
* (RUSTSEC-2023-0071), overwritten by `rsa 0.10.0-rc.18` — and the direct
|
|
21
|
+
* `minimist@1.2.0` was reported as its nested `0.0.8`.
|
|
22
|
+
* - `versions`: the ONE version per name a direct dependency resolves to (the
|
|
23
|
+
* top-level npm copy, the crate version a workspace member depends on),
|
|
24
|
+
* which dependency health and upgrade planning read.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { readBunLock } from "./bun-lockfile.js";
|
|
29
|
+
import { readPnpmLock, readYarnLock } from "./js-lockfile-readers.js";
|
|
30
|
+
import { resolveGo, resolvePython } from "./lockfile-parsers-pygo.js";
|
|
31
|
+
import { emptySource, found, InstanceSet } from "./lockfile-types.js";
|
|
32
|
+
const LOCKFILES = {
|
|
33
|
+
npm: ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"],
|
|
34
|
+
"crates.io": ["Cargo.lock"],
|
|
35
|
+
PyPI: ["poetry.lock", "uv.lock", "Pipfile.lock"],
|
|
36
|
+
Go: ["go.mod", "go.sum"],
|
|
37
|
+
};
|
|
38
|
+
const MANIFESTS = {
|
|
39
|
+
npm: "package.json",
|
|
40
|
+
"crates.io": "Cargo.toml",
|
|
41
|
+
PyPI: "requirements.txt",
|
|
42
|
+
};
|
|
43
|
+
/** Every lock file the resolver may read for this ecosystem, present or not, in priority order. */
|
|
44
|
+
export function lockfileCandidates(cwd, ecosystem) {
|
|
45
|
+
return (LOCKFILES[ecosystem] ?? []).map((name) => path.join(cwd, name));
|
|
46
|
+
}
|
|
47
|
+
/** The manifest the resolver falls back to for this ecosystem, or null when it has none. */
|
|
48
|
+
export function manifestCandidate(cwd, ecosystem) {
|
|
49
|
+
const name = MANIFESTS[ecosystem];
|
|
50
|
+
return name ? path.join(cwd, name) : null;
|
|
51
|
+
}
|
|
52
|
+
export function resolveVersionSource(cwd, ecosystem) {
|
|
53
|
+
switch (ecosystem) {
|
|
54
|
+
case "npm":
|
|
55
|
+
return resolveNpm(cwd);
|
|
56
|
+
case "crates.io":
|
|
57
|
+
return resolveRust(cwd);
|
|
58
|
+
case "PyPI":
|
|
59
|
+
return resolvePython(cwd);
|
|
60
|
+
case "Go":
|
|
61
|
+
return resolveGo(cwd);
|
|
62
|
+
default:
|
|
63
|
+
return emptySource();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Instances for a manifest-only read: one per name, scope unknown. */
|
|
67
|
+
function manifestInstances(versions) {
|
|
68
|
+
return [...versions].map(([name, version]) => ({ name, version }));
|
|
69
|
+
}
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// npm: package-lock.json > npm-shrinkwrap.json > pnpm-lock.yaml > yarn.lock > bun.lock > package.json ranges
|
|
72
|
+
// =============================================================================
|
|
73
|
+
function resolveNpm(cwd) {
|
|
74
|
+
for (const name of ["package-lock.json", "npm-shrinkwrap.json"]) {
|
|
75
|
+
const lockPath = path.join(cwd, name);
|
|
76
|
+
if (!fs.existsSync(lockPath))
|
|
77
|
+
continue;
|
|
78
|
+
try {
|
|
79
|
+
const read = readPackageLock(JSON.parse(fs.readFileSync(lockPath, "utf-8")));
|
|
80
|
+
if (read.instances.size > 0)
|
|
81
|
+
return found(read.versions, read.instances, lockPath, "lockfile");
|
|
82
|
+
}
|
|
83
|
+
catch { /* fall through */ }
|
|
84
|
+
}
|
|
85
|
+
// pnpm-lock.yaml (v5.x, v6 and v9, read by column; see js-lockfile-readers.ts)
|
|
86
|
+
const pnpmLockPath = path.join(cwd, "pnpm-lock.yaml");
|
|
87
|
+
if (fs.existsSync(pnpmLockPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const versions = new Map();
|
|
90
|
+
const instances = new InstanceSet();
|
|
91
|
+
readPnpmLock(fs.readFileSync(pnpmLockPath, "utf-8"), versions, instances);
|
|
92
|
+
if (versions.size > 0)
|
|
93
|
+
return found(versions, instances, pnpmLockPath, "lockfile");
|
|
94
|
+
}
|
|
95
|
+
catch { /* fall through */ }
|
|
96
|
+
}
|
|
97
|
+
// yarn.lock (v1 and berry)
|
|
98
|
+
const yarnLockPath = path.join(cwd, "yarn.lock");
|
|
99
|
+
if (fs.existsSync(yarnLockPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const versions = new Map();
|
|
102
|
+
const instances = new InstanceSet();
|
|
103
|
+
const specs = new Map();
|
|
104
|
+
readYarnLock(fs.readFileSync(yarnLockPath, "utf-8"), versions, instances, specs);
|
|
105
|
+
// The copy a direct dependency resolves to is the one its package.json range names.
|
|
106
|
+
for (const [name, range] of manifestRanges(cwd)) {
|
|
107
|
+
const version = specs.get(`${name}@${range}`);
|
|
108
|
+
if (version)
|
|
109
|
+
versions.set(name, version);
|
|
110
|
+
}
|
|
111
|
+
if (versions.size > 0)
|
|
112
|
+
return found(versions, instances, yarnLockPath, "lockfile");
|
|
113
|
+
}
|
|
114
|
+
catch { /* fall through */ }
|
|
115
|
+
}
|
|
116
|
+
// bun.lock (Bun 1.2+ text lockfile; the binary bun.lockb is not readable)
|
|
117
|
+
const bunLockPath = path.join(cwd, "bun.lock");
|
|
118
|
+
if (fs.existsSync(bunLockPath)) {
|
|
119
|
+
try {
|
|
120
|
+
const versions = new Map();
|
|
121
|
+
const instances = new InstanceSet();
|
|
122
|
+
readBunLock(fs.readFileSync(bunLockPath, "utf-8"), versions, instances);
|
|
123
|
+
if (versions.size > 0)
|
|
124
|
+
return found(versions, instances, bunLockPath, "lockfile");
|
|
125
|
+
}
|
|
126
|
+
catch { /* fall through */ }
|
|
127
|
+
}
|
|
128
|
+
// Fallback: extract version floors from package.json ranges
|
|
129
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
130
|
+
if (fs.existsSync(pkgPath)) {
|
|
131
|
+
const versions = new Map();
|
|
132
|
+
try {
|
|
133
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
134
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
135
|
+
for (const [name, spec] of Object.entries(allDeps)) {
|
|
136
|
+
const version = extractVersionFromSpec(spec);
|
|
137
|
+
if (version)
|
|
138
|
+
versions.set(name, version);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { /* skip */ }
|
|
142
|
+
return found(versions, manifestInstances(versions), pkgPath, "manifest");
|
|
143
|
+
}
|
|
144
|
+
return emptySource();
|
|
145
|
+
}
|
|
146
|
+
/** The package name a package-lock `packages` key installs: the segment after its LAST `node_modules/`. */
|
|
147
|
+
export function packageNameFromNodeModulesPath(lockPath) {
|
|
148
|
+
const marker = "node_modules/";
|
|
149
|
+
const index = lockPath.lastIndexOf(marker);
|
|
150
|
+
return index === -1 ? "" : lockPath.slice(index + marker.length);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* package-lock.json / npm-shrinkwrap.json, lockfile v1, v2 and v3.
|
|
154
|
+
*
|
|
155
|
+
* v2/v3 `packages` keys are install paths: `node_modules/a` is the hoisted
|
|
156
|
+
* copy a direct dependency of the root resolves to, `node_modules/a/node_modules/b`
|
|
157
|
+
* a nested one. npm writes `dev: true` on every entry only reachable from
|
|
158
|
+
* devDependencies (`devOptional` = also reachable as an optional runtime dep,
|
|
159
|
+
* so runtime), which makes the dev scope of a TRANSITIVE known. v1 nests the
|
|
160
|
+
* same information under `dependencies`.
|
|
161
|
+
*/
|
|
162
|
+
export function readPackageLock(lock) {
|
|
163
|
+
const versions = new Map();
|
|
164
|
+
const instances = new InstanceSet();
|
|
165
|
+
if (lock.packages) {
|
|
166
|
+
for (const [key, value] of Object.entries(lock.packages)) {
|
|
167
|
+
const name = packageNameFromNodeModulesPath(key);
|
|
168
|
+
if (!name || !value || value.link || !value.version)
|
|
169
|
+
continue;
|
|
170
|
+
instances.add(name, value.version, value.dev === true);
|
|
171
|
+
// The hoisted root copy is what `require(name)` from the project resolves to.
|
|
172
|
+
if (key === `node_modules/${name}`)
|
|
173
|
+
versions.set(name, value.version);
|
|
174
|
+
}
|
|
175
|
+
// A workspace package's own nested copy (packages/x/node_modules/a) when nothing is hoisted.
|
|
176
|
+
for (const instance of instances.toArray()) {
|
|
177
|
+
if (!versions.has(instance.name))
|
|
178
|
+
versions.set(instance.name, instance.version);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (instances.size === 0 && lock.dependencies) {
|
|
182
|
+
const walk = (deps, topLevel) => {
|
|
183
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
184
|
+
if (value?.version && /^\d/.test(value.version)) {
|
|
185
|
+
instances.add(name, value.version, value.dev === true);
|
|
186
|
+
if (topLevel)
|
|
187
|
+
versions.set(name, value.version);
|
|
188
|
+
}
|
|
189
|
+
if (value?.dependencies)
|
|
190
|
+
walk(value.dependencies, false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
walk(lock.dependencies, true);
|
|
194
|
+
}
|
|
195
|
+
return { versions, instances };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Every `[[package]]` table in a Cargo.lock, read field by field. The old
|
|
199
|
+
* reader required `name` and `version` on consecutive lines; that holds for
|
|
200
|
+
* cargo's own writer, but the table is TOML and nothing else guarantees it.
|
|
201
|
+
*/
|
|
202
|
+
export function parseCargoLockPackages(content) {
|
|
203
|
+
const packages = [];
|
|
204
|
+
for (const block of content.split(/^\[\[package\]\]\s*$/m).slice(1)) {
|
|
205
|
+
const body = block.split(/^\[(?!\[package\]\])/m)[0];
|
|
206
|
+
const field = (key) => new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m").exec(body)?.[1] ?? null;
|
|
207
|
+
const name = field("name");
|
|
208
|
+
const version = field("version");
|
|
209
|
+
if (!name || !version)
|
|
210
|
+
continue;
|
|
211
|
+
const depsMatch = /^dependencies\s*=\s*\[([\s\S]*?)\]/m.exec(body);
|
|
212
|
+
const dependencies = depsMatch ? [...depsMatch[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]) : [];
|
|
213
|
+
packages.push({ name, version, source: field("source"), dependencies });
|
|
214
|
+
}
|
|
215
|
+
return packages;
|
|
216
|
+
}
|
|
217
|
+
function resolveRust(cwd) {
|
|
218
|
+
const lockPath = path.join(cwd, "Cargo.lock");
|
|
219
|
+
if (!fs.existsSync(lockPath)) {
|
|
220
|
+
return resolveRustFromManifest(cwd);
|
|
221
|
+
}
|
|
222
|
+
const versions = new Map();
|
|
223
|
+
const instances = new InstanceSet();
|
|
224
|
+
try {
|
|
225
|
+
const packages = parseCargoLockPackages(fs.readFileSync(lockPath, "utf-8"));
|
|
226
|
+
const versionsByName = new Map();
|
|
227
|
+
// Cargo writes `source` on every registry or git package. In a file it
|
|
228
|
+
// wrote, a package with none is a workspace member or path dependency:
|
|
229
|
+
// local code, not a release, so no advisory can be about it. A lockfile
|
|
230
|
+
// with no `source` anywhere was not written by cargo; keep everything.
|
|
231
|
+
const cargoWritten = packages.some((pkg) => pkg.source !== null);
|
|
232
|
+
const isLocal = (pkg) => cargoWritten && pkg.source === null;
|
|
233
|
+
for (const pkg of packages) {
|
|
234
|
+
if (isLocal(pkg))
|
|
235
|
+
continue;
|
|
236
|
+
instances.add(pkg.name, pkg.version);
|
|
237
|
+
versionsByName.set(pkg.name, [...(versionsByName.get(pkg.name) ?? []), pkg.version]);
|
|
238
|
+
}
|
|
239
|
+
// The version a direct dependency resolves to: the one a workspace member
|
|
240
|
+
// names. Cargo writes "name version" when two versions coexist, else "name".
|
|
241
|
+
for (const member of packages.filter(isLocal)) {
|
|
242
|
+
for (const dep of member.dependencies) {
|
|
243
|
+
const [name, version] = dep.split(" ");
|
|
244
|
+
const candidates = versionsByName.get(name);
|
|
245
|
+
if (!candidates || versions.has(name))
|
|
246
|
+
continue;
|
|
247
|
+
if (version && candidates.includes(version))
|
|
248
|
+
versions.set(name, version);
|
|
249
|
+
else if (!version && candidates.length === 1)
|
|
250
|
+
versions.set(name, candidates[0]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for (const [name, list] of versionsByName) {
|
|
254
|
+
if (!versions.has(name))
|
|
255
|
+
versions.set(name, list[0]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch { /* skip */ }
|
|
259
|
+
return found(versions, instances, lockPath, "lockfile");
|
|
260
|
+
}
|
|
261
|
+
function resolveRustFromManifest(cwd) {
|
|
262
|
+
const versions = new Map();
|
|
263
|
+
const cargoPath = path.join(cwd, "Cargo.toml");
|
|
264
|
+
if (!fs.existsSync(cargoPath))
|
|
265
|
+
return emptySource();
|
|
266
|
+
try {
|
|
267
|
+
const content = fs.readFileSync(cargoPath, "utf-8");
|
|
268
|
+
// name = "1.0" or name = { version = "1.0", ... }
|
|
269
|
+
const depRegex = /^([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]+)"|.*?version\s*=\s*"([^"]+)")/gm;
|
|
270
|
+
let match;
|
|
271
|
+
while ((match = depRegex.exec(content)) !== null) {
|
|
272
|
+
const name = match[1];
|
|
273
|
+
const version = match[2] || match[3];
|
|
274
|
+
if (version && /^\d/.test(version)) {
|
|
275
|
+
versions.set(name, version);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { /* skip */ }
|
|
280
|
+
return found(versions, manifestInstances(versions), cargoPath, "manifest");
|
|
281
|
+
}
|
|
282
|
+
// =============================================================================
|
|
283
|
+
// Helpers
|
|
284
|
+
// =============================================================================
|
|
285
|
+
/** package.json dependency ranges (all dependency fields), or none when unreadable. */
|
|
286
|
+
function manifestRanges(cwd) {
|
|
287
|
+
try {
|
|
288
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
291
|
+
for (const [name, range] of Object.entries(pkg[field] ?? {})) {
|
|
292
|
+
if (typeof range === "string")
|
|
293
|
+
out.push([name, range]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Extract a usable version from an npm version specifier.
|
|
304
|
+
* "^4.18.2" -> "4.18.2", "~1.0.0" -> "1.0.0", ">=2.0.0" -> "2.0.0"
|
|
305
|
+
*/
|
|
306
|
+
function extractVersionFromSpec(spec) {
|
|
307
|
+
if (!spec)
|
|
308
|
+
return null;
|
|
309
|
+
const match = spec.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
|
|
310
|
+
return match ? match[1] : null;
|
|
311
|
+
}
|