@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,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Install drift: what node_modules holds versus what the lockfile pins.
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-09-10: mcp-4da-server/pnpm-lock.yaml pinned hono 4.13.5 (PR
|
|
6
|
+
* #632, the fix for CVE-2026-84363/-84364/-84365) while
|
|
7
|
+
* mcp-4da-server/node_modules/hono held 4.13.1 for 25 days, because
|
|
8
|
+
* activation ran `pnpm install` only at the repo root. Every 4DA surface
|
|
9
|
+
* reported hono fixed, because every surface read the lockfile. A lockfile
|
|
10
|
+
* says what SHOULD be installed; only node_modules says what IS, and the
|
|
11
|
+
* installed copy is the one that runs.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { fileSignature } from "./file-signature.js";
|
|
16
|
+
/** Parent directories the lookup may climb for hoisted workspaces. */
|
|
17
|
+
const MAX_PARENT_LEVELS = 6;
|
|
18
|
+
/**
|
|
19
|
+
* Files an install rewrites (pnpm, npm >= 7, yarn 1, yarn berry's
|
|
20
|
+
* node-modules linker). A change to one is how a reinstall that fixes drift,
|
|
21
|
+
* without touching the lockfile, gets noticed.
|
|
22
|
+
*/
|
|
23
|
+
const INSTALL_MARKERS = [".modules.yaml", ".package-lock.json", ".yarn-integrity", ".yarn-state.yml"];
|
|
24
|
+
/** The reinstall command for the lockfile a directory resolved from, or null for a non-npm file. */
|
|
25
|
+
export function installFixFor(lockfilePath) {
|
|
26
|
+
switch (path.basename(lockfilePath)) {
|
|
27
|
+
case "pnpm-lock.yaml":
|
|
28
|
+
return "pnpm install";
|
|
29
|
+
case "package-lock.json":
|
|
30
|
+
return "npm ci";
|
|
31
|
+
case "yarn.lock":
|
|
32
|
+
return "yarn install";
|
|
33
|
+
case "bun.lock":
|
|
34
|
+
return "bun install";
|
|
35
|
+
default:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function isDirectory(p) {
|
|
40
|
+
try {
|
|
41
|
+
return fs.statSync(p).isDirectory();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The directories a lookup from `dir` visits, nearest first: `dir`, then its
|
|
49
|
+
* parents, stopping at the directory that holds `.git` (the repository root)
|
|
50
|
+
* or after MAX_PARENT_LEVELS parents.
|
|
51
|
+
*/
|
|
52
|
+
function lookupChain(dir) {
|
|
53
|
+
const chain = [];
|
|
54
|
+
let current = path.resolve(dir);
|
|
55
|
+
for (let level = 0; level <= MAX_PARENT_LEVELS; level++) {
|
|
56
|
+
chain.push(current);
|
|
57
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
58
|
+
break;
|
|
59
|
+
const parent = path.dirname(current);
|
|
60
|
+
if (parent === current)
|
|
61
|
+
break;
|
|
62
|
+
current = parent;
|
|
63
|
+
}
|
|
64
|
+
return chain;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `<d>/node_modules/<name>/package.json` for the nearest `d` in the lookup
|
|
68
|
+
* chain that has it (scoped names included), or null. This is the copy
|
|
69
|
+
* Node's resolver would load from `dir`.
|
|
70
|
+
*/
|
|
71
|
+
export function findInstalledManifest(dir, name, chain = lookupChain(dir)) {
|
|
72
|
+
for (const d of chain) {
|
|
73
|
+
const candidate = path.join(d, "node_modules", ...name.split("/"), "package.json");
|
|
74
|
+
if (fs.existsSync(candidate))
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function readManifestVersion(manifest) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
82
|
+
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
83
|
+
return version || null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The installed version of `name` as seen from `dir`, or null when not installed or unreadable. Never throws. */
|
|
90
|
+
export function readInstalledVersion(dir, name) {
|
|
91
|
+
const manifest = findInstalledManifest(dir, name);
|
|
92
|
+
return manifest ? readManifestVersion(manifest) : null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compare each npm direct dependency's lockfile version with node_modules.
|
|
96
|
+
*
|
|
97
|
+
* Skipped entirely when `dir` has no node_modules of its own: a checkout that
|
|
98
|
+
* was never installed has no installed copy to disagree with the lockfile,
|
|
99
|
+
* and borrowing a parent project's node_modules would compare against a
|
|
100
|
+
* different project's install. The node_modules path is still tracked, so an
|
|
101
|
+
* install that creates it is noticed.
|
|
102
|
+
*/
|
|
103
|
+
export function checkInstallState(dir, resolved, lockfilePath) {
|
|
104
|
+
const nodeModules = path.join(dir, "node_modules");
|
|
105
|
+
const signatures = new Map([[nodeModules, fileSignature(nodeModules)]]);
|
|
106
|
+
const fix = installFixFor(lockfilePath);
|
|
107
|
+
if (!fix || !isDirectory(nodeModules)) {
|
|
108
|
+
return { resolved, drift: [], driftAuditDeps: [], signatures };
|
|
109
|
+
}
|
|
110
|
+
const chain = lookupChain(dir);
|
|
111
|
+
for (const d of chain) {
|
|
112
|
+
const nm = path.join(d, "node_modules");
|
|
113
|
+
if (!isDirectory(nm))
|
|
114
|
+
continue;
|
|
115
|
+
signatures.set(nm, fileSignature(nm));
|
|
116
|
+
for (const marker of INSTALL_MARKERS) {
|
|
117
|
+
const markerPath = path.join(nm, marker);
|
|
118
|
+
signatures.set(markerPath, fileSignature(markerPath));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const drift = [];
|
|
122
|
+
const driftAuditDeps = [];
|
|
123
|
+
const checked = resolved.map((dep) => {
|
|
124
|
+
if (!dep.isDirect || dep.ecosystem !== "npm" || !dep.version)
|
|
125
|
+
return dep;
|
|
126
|
+
const manifest = findInstalledManifest(dir, dep.name, chain);
|
|
127
|
+
if (manifest)
|
|
128
|
+
signatures.set(manifest, fileSignature(manifest));
|
|
129
|
+
const installedVersion = manifest ? readManifestVersion(manifest) : null;
|
|
130
|
+
if (installedVersion && installedVersion !== dep.version) {
|
|
131
|
+
drift.push({
|
|
132
|
+
package: dep.name,
|
|
133
|
+
dir,
|
|
134
|
+
lockfileVersion: dep.version,
|
|
135
|
+
installedVersion,
|
|
136
|
+
fix,
|
|
137
|
+
isDev: dep.isDev,
|
|
138
|
+
});
|
|
139
|
+
driftAuditDeps.push({
|
|
140
|
+
...dep,
|
|
141
|
+
version: installedVersion,
|
|
142
|
+
installedVersion,
|
|
143
|
+
installDriftOf: dep.version,
|
|
144
|
+
installFix: fix,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { ...dep, installedVersion };
|
|
148
|
+
});
|
|
149
|
+
return { resolved: checked, drift, driftAuditDeps, signatures };
|
|
150
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pnpm and yarn lockfile readers: exact versions, no YAML parser.
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-09-11: the previous pnpm reader matched package keys with
|
|
5
|
+
* `/^\s{2}'?(.+?)@(\d…)/gm` over the whole file. `\s` also matches a newline,
|
|
6
|
+
* and pnpm writes a blank line between entries, so each match began on the
|
|
7
|
+
* blank line and kept the next key's opening quote. Every scoped package
|
|
8
|
+
* outside the importer blocks reached OSV as `'@scope/name` and matched
|
|
9
|
+
* nothing: 525 names across five lockfiles in this repository, including
|
|
10
|
+
* `@humanfs/node@0.16.7` (GHSA-p498-v437-472g), which GitHub and the app both
|
|
11
|
+
* flagged. The test fixture had no blank lines, so it passed. The yarn reader's
|
|
12
|
+
* header pattern could not start with `@`, so it dropped every scoped package,
|
|
13
|
+
* and it never read yarn berry's `version: x` lines.
|
|
14
|
+
*
|
|
15
|
+
* So indentation is read by column on whole lines, never with `\s` across a
|
|
16
|
+
* line break, and a key that does not pin a registry version (git, tarball,
|
|
17
|
+
* file, link) is dropped instead of being queried as a package name.
|
|
18
|
+
*/
|
|
19
|
+
import type { InstanceSet } from "./lockfile-types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Splits one `packages:` or `snapshots:` key into [name, version]:
|
|
22
|
+
* v9 `'@scope/pkg@1.2.3(peer@4.5.6)'`, v6 `/pkg@1.2.3`, v5 `/@scope/pkg/1.2.3_peer@4.5.6`.
|
|
23
|
+
* Returns null for a key that does not pin a registry version.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parsePnpmKey(rawKey: string): [string, string] | null;
|
|
26
|
+
/**
|
|
27
|
+
* Reads a pnpm lockfile (v5.x, v6 or v9) into `versions`; the first version seen wins.
|
|
28
|
+
*
|
|
29
|
+
* Direct pins are read first, from the dependency blocks: `importers:` → ` <dir>:` →
|
|
30
|
+
* ` dependencies:` in a workspace, or a top-level `dependencies:` block in a v5/v6
|
|
31
|
+
* single-project file. A direct pin therefore wins over a transitive copy of the same
|
|
32
|
+
* name. Every other package comes from a key exactly two columns deep under
|
|
33
|
+
* `packages:` or `snapshots:`.
|
|
34
|
+
*
|
|
35
|
+
* `instances`, when given, receives EVERY copy (a package keyed at two versions is
|
|
36
|
+
* two copies), with the `dev: true|false` that v5/v6 lockfiles write under each
|
|
37
|
+
* package key. pnpm 9 dropped that field, so its copies have unknown scope.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readPnpmLock(content: string, versions: Map<string, string>, instances?: InstanceSet): void;
|
|
40
|
+
/**
|
|
41
|
+
* Reads a yarn lockfile (v1 or berry) into `versions`.
|
|
42
|
+
* v1: `"@scope/pkg@^1.0.0", "@scope/pkg@^1.1.0":` then ` version "1.2.3"`.
|
|
43
|
+
* berry: `"@scope/pkg@npm:^1.0.0":` then ` version: 1.2.3`.
|
|
44
|
+
* Workspace, link, portal, file, exec and patch entries do not name a registry version.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readYarnLock(content: string, versions: Map<string, string>, instances?: InstanceSet, specs?: Map<string, string>): void;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* pnpm and yarn lockfile readers: exact versions, no YAML parser.
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-09-11: the previous pnpm reader matched package keys with
|
|
6
|
+
* `/^\s{2}'?(.+?)@(\d…)/gm` over the whole file. `\s` also matches a newline,
|
|
7
|
+
* and pnpm writes a blank line between entries, so each match began on the
|
|
8
|
+
* blank line and kept the next key's opening quote. Every scoped package
|
|
9
|
+
* outside the importer blocks reached OSV as `'@scope/name` and matched
|
|
10
|
+
* nothing: 525 names across five lockfiles in this repository, including
|
|
11
|
+
* `@humanfs/node@0.16.7` (GHSA-p498-v437-472g), which GitHub and the app both
|
|
12
|
+
* flagged. The test fixture had no blank lines, so it passed. The yarn reader's
|
|
13
|
+
* header pattern could not start with `@`, so it dropped every scoped package,
|
|
14
|
+
* and it never read yarn berry's `version: x` lines.
|
|
15
|
+
*
|
|
16
|
+
* So indentation is read by column on whole lines, never with `\s` across a
|
|
17
|
+
* line break, and a key that does not pin a registry version (git, tarball,
|
|
18
|
+
* file, link) is dropped instead of being queried as a package name.
|
|
19
|
+
*/
|
|
20
|
+
const DEPENDENCY_BLOCKS = new Set(["dependencies:", "devDependencies:", "optionalDependencies:"]);
|
|
21
|
+
/** A bare npm package name, scoped or not: no whitespace, no `/` beyond the scope, no version. */
|
|
22
|
+
const PACKAGE_NAME = /^(?:@[^\s/@]+\/)?[^\s/@]+$/;
|
|
23
|
+
function unquote(s) {
|
|
24
|
+
return s.length >= 2 && (s[0] === "'" || s[0] === '"') && s[s.length - 1] === s[0] ? s.slice(1, -1) : s;
|
|
25
|
+
}
|
|
26
|
+
/** An exact version with pnpm's peer suffixes removed: v6/v9 `1.2.3(react@18)`, v5 `1.2.3_react@18`. */
|
|
27
|
+
function exactVersion(raw) {
|
|
28
|
+
const version = unquote(raw.trim()).split("(")[0].split("_")[0].trim();
|
|
29
|
+
return /^\d/.test(version) ? version : null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Splits one `packages:` or `snapshots:` key into [name, version]:
|
|
33
|
+
* v9 `'@scope/pkg@1.2.3(peer@4.5.6)'`, v6 `/pkg@1.2.3`, v5 `/@scope/pkg/1.2.3_peer@4.5.6`.
|
|
34
|
+
* Returns null for a key that does not pin a registry version.
|
|
35
|
+
*/
|
|
36
|
+
export function parsePnpmKey(rawKey) {
|
|
37
|
+
let key = unquote(rawKey.trim());
|
|
38
|
+
if (key.startsWith("/"))
|
|
39
|
+
key = key.slice(1);
|
|
40
|
+
const peers = key.indexOf("(");
|
|
41
|
+
if (peers >= 0)
|
|
42
|
+
key = key.slice(0, peers);
|
|
43
|
+
// v6 and v9: the separator is the first `@` after a scoped name's own leading `@`.
|
|
44
|
+
const at = key.indexOf("@", key.startsWith("@") ? 1 : 0);
|
|
45
|
+
if (at > 0) {
|
|
46
|
+
const name = key.slice(0, at);
|
|
47
|
+
const version = exactVersion(key.slice(at + 1));
|
|
48
|
+
if (PACKAGE_NAME.test(name) && version)
|
|
49
|
+
return [name, version];
|
|
50
|
+
}
|
|
51
|
+
// v5: `name/version`, with any peers after an underscore.
|
|
52
|
+
const v5 = /^((?:@[^/@\s]+\/)?[^/@\s]+)\/(\d[^/]*)$/.exec(key);
|
|
53
|
+
if (v5) {
|
|
54
|
+
const version = exactVersion(v5[2]);
|
|
55
|
+
if (version)
|
|
56
|
+
return [v5[1], version];
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads a pnpm lockfile (v5.x, v6 or v9) into `versions`; the first version seen wins.
|
|
62
|
+
*
|
|
63
|
+
* Direct pins are read first, from the dependency blocks: `importers:` → ` <dir>:` →
|
|
64
|
+
* ` dependencies:` in a workspace, or a top-level `dependencies:` block in a v5/v6
|
|
65
|
+
* single-project file. A direct pin therefore wins over a transitive copy of the same
|
|
66
|
+
* name. Every other package comes from a key exactly two columns deep under
|
|
67
|
+
* `packages:` or `snapshots:`.
|
|
68
|
+
*
|
|
69
|
+
* `instances`, when given, receives EVERY copy (a package keyed at two versions is
|
|
70
|
+
* two copies), with the `dev: true|false` that v5/v6 lockfiles write under each
|
|
71
|
+
* package key. pnpm 9 dropped that field, so its copies have unknown scope.
|
|
72
|
+
*/
|
|
73
|
+
export function readPnpmLock(content, versions, instances) {
|
|
74
|
+
const transitive = [];
|
|
75
|
+
const devByKey = new Map();
|
|
76
|
+
const setFirst = (name, version) => {
|
|
77
|
+
if (!versions.has(name))
|
|
78
|
+
versions.set(name, version);
|
|
79
|
+
};
|
|
80
|
+
let section = "";
|
|
81
|
+
let blockIndent = -1; // column of the dependency block being read, or -1
|
|
82
|
+
let pending = null; // v6/v9: a name whose `version:` line follows
|
|
83
|
+
let packageKey = null; // the packages/snapshots key whose fields are being read
|
|
84
|
+
for (const line of content.split(/\r?\n/)) {
|
|
85
|
+
const text = line.trim();
|
|
86
|
+
if (text === "" || text.startsWith("#"))
|
|
87
|
+
continue;
|
|
88
|
+
const indent = line.length - line.trimStart().length;
|
|
89
|
+
if (indent === 0) {
|
|
90
|
+
section = text.endsWith(":") ? text.slice(0, -1) : "";
|
|
91
|
+
blockIndent = DEPENDENCY_BLOCKS.has(text) ? 0 : -1;
|
|
92
|
+
pending = null;
|
|
93
|
+
packageKey = null;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (section === "packages" || section === "snapshots") {
|
|
97
|
+
if (indent === 2 && text.endsWith(":")) {
|
|
98
|
+
const parsed = parsePnpmKey(text.slice(0, -1));
|
|
99
|
+
packageKey = parsed ? `${parsed[0]}\0${parsed[1]}` : null;
|
|
100
|
+
if (parsed)
|
|
101
|
+
transitive.push(parsed);
|
|
102
|
+
}
|
|
103
|
+
else if (indent === 4 && packageKey) {
|
|
104
|
+
const dev = /^dev:\s*(true|false)\s*$/.exec(text);
|
|
105
|
+
if (dev)
|
|
106
|
+
devByKey.set(packageKey, dev[1] === "true");
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (section === "importers" && indent <= 4) {
|
|
111
|
+
blockIndent = indent === 4 && DEPENDENCY_BLOCKS.has(text) ? 4 : -1;
|
|
112
|
+
pending = null;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (blockIndent < 0)
|
|
116
|
+
continue;
|
|
117
|
+
if (indent === blockIndent + 2) {
|
|
118
|
+
pending = null;
|
|
119
|
+
const entry = /^(?:'([^']+)'|"([^"]+)"|([^\s:'"]+)):(?:\s+(.*))?$/.exec(text);
|
|
120
|
+
if (!entry)
|
|
121
|
+
continue;
|
|
122
|
+
const name = entry[1] ?? entry[2] ?? entry[3];
|
|
123
|
+
const value = entry[4];
|
|
124
|
+
if (value === undefined || value === "") {
|
|
125
|
+
pending = name;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const version = exactVersion(value); // v5: `name: 1.2.3_peer@4.5.6`
|
|
129
|
+
if (version)
|
|
130
|
+
setFirst(name, version);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else if (indent === blockIndent + 4 && pending && text.startsWith("version:")) {
|
|
134
|
+
const version = exactVersion(text.slice("version:".length));
|
|
135
|
+
if (version)
|
|
136
|
+
setFirst(pending, version);
|
|
137
|
+
pending = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const [name, version] of transitive)
|
|
141
|
+
setFirst(name, version);
|
|
142
|
+
if (!instances)
|
|
143
|
+
return;
|
|
144
|
+
for (const [name, version] of versions) {
|
|
145
|
+
if (!transitive.some(([n, v]) => n === name && v === version))
|
|
146
|
+
instances.add(name, version);
|
|
147
|
+
}
|
|
148
|
+
for (const [name, version] of transitive)
|
|
149
|
+
instances.add(name, version, devByKey.get(`${name}\0${version}`));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Reads a yarn lockfile (v1 or berry) into `versions`.
|
|
153
|
+
* v1: `"@scope/pkg@^1.0.0", "@scope/pkg@^1.1.0":` then ` version "1.2.3"`.
|
|
154
|
+
* berry: `"@scope/pkg@npm:^1.0.0":` then ` version: 1.2.3`.
|
|
155
|
+
* Workspace, link, portal, file, exec and patch entries do not name a registry version.
|
|
156
|
+
*/
|
|
157
|
+
export function readYarnLock(content, versions, instances, specs) {
|
|
158
|
+
for (const block of content.split(/\r?\n(?=\S)/)) {
|
|
159
|
+
const firstLine = block.split(/\r?\n/)[0];
|
|
160
|
+
const header = /^"?(@?[^@\s"]+)@/.exec(firstLine);
|
|
161
|
+
if (!header || header[1].startsWith("__"))
|
|
162
|
+
continue;
|
|
163
|
+
if (/@(?:workspace|link|portal|file|exec|patch):/.test(firstLine))
|
|
164
|
+
continue;
|
|
165
|
+
const line = /^\s+version:?\s+"?([^"\s]+)"?\s*$/m.exec(block);
|
|
166
|
+
const version = line ? exactVersion(line[1]) : null;
|
|
167
|
+
if (!version)
|
|
168
|
+
continue;
|
|
169
|
+
// One block per resolved version. Blocks are sorted by spec, so the first
|
|
170
|
+
// block of a name can be an old nested copy; `specs` lets the caller pick
|
|
171
|
+
// the copy a package.json range resolves to instead.
|
|
172
|
+
if (!versions.has(header[1]))
|
|
173
|
+
versions.set(header[1], version);
|
|
174
|
+
instances?.add(header[1], version);
|
|
175
|
+
if (specs) {
|
|
176
|
+
for (const spec of firstLine.replace(/:\s*$/, "").split(",")) {
|
|
177
|
+
specs.set(unquote(spec.trim()).replace(/@npm:/, "@"), version);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python and Go lockfile parsers (split out of lockfile-parsers.ts).
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-10-02 against osv-scanner and pip-audit, every miss below was
|
|
5
|
+
* a parsing loss before OSV was ever asked:
|
|
6
|
+
* - requirements.txt: `urllib3[secure]==1.25.0` and dotted names were skipped,
|
|
7
|
+
* and an inline comment rode along as part of the version — OSV was asked
|
|
8
|
+
* about `"8.0.0 # imaging"` and answered with 63 advisories for the real 45.
|
|
9
|
+
* - poetry.lock: the reader required `name` and `version` on consecutive
|
|
10
|
+
* lines. Poetry before 1.0 wrote its keys alphabetically (`category`,
|
|
11
|
+
* `description`, `name`, ..., `version`), so a whole lockfile read as empty.
|
|
12
|
+
* - go.sum: `(v[\d.]+)` cut `v0.0.0-20200622213623-75b288015ac9` to `v0.0.0`
|
|
13
|
+
* and dropped `+incompatible`; `v0.0.0` then sorted AFTER the real
|
|
14
|
+
* pseudo-version, so older advisories were missed and later ones invented.
|
|
15
|
+
* go.sum also lists versions the build never selected.
|
|
16
|
+
*/
|
|
17
|
+
import { InstanceSet, type VersionSource } from "./lockfile-types.js";
|
|
18
|
+
/** PEP 503 normalised name: lowercase, runs of `-`, `_`, `.` become one `-`. */
|
|
19
|
+
export declare function normalizePythonName(name: string): string;
|
|
20
|
+
export declare function resolvePython(cwd: string): VersionSource;
|
|
21
|
+
/**
|
|
22
|
+
* [[package]] tables from poetry.lock (any version) or uv.lock. A pre-1.2
|
|
23
|
+
* poetry.lock records `category = "dev"` for dev-only packages; newer files
|
|
24
|
+
* and uv.lock do not record scope here, so it stays unknown.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readTomlPackageLock(content: string): {
|
|
27
|
+
versions: Map<string, string>;
|
|
28
|
+
instances: InstanceSet;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Exact pins from a requirements file: `name==1.2`, `name===1.2`,
|
|
32
|
+
* `name[extra1,extra2]==1.2`, with environment markers and inline comments.
|
|
33
|
+
* Ranges (`>=`, `~=`) do not name an installed version and are skipped, as are
|
|
34
|
+
* options (`-r`, `-e`, `--hash`) and URL requirements.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseRequirements(content: string): Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
version: string;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function resolveGo(cwd: string): VersionSource;
|
|
41
|
+
/** `require` lines (block and single form) with `replace` applied, and whether the go directive is >= 1.17. */
|
|
42
|
+
export declare function parseGoMod(content: string): {
|
|
43
|
+
requires: Array<{
|
|
44
|
+
module: string;
|
|
45
|
+
version: string;
|
|
46
|
+
indirect: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
goVersionAtLeast117: boolean;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Per module, the highest version go.sum records — minimal version selection
|
|
52
|
+
* picks the highest required version, and go.sum records every version the
|
|
53
|
+
* module graph required. A module listed only by its `/go.mod` hash is still
|
|
54
|
+
* in Go's build list (`go list -m all`): measured 2026-10-02 on gh cli v1.0
|
|
55
|
+
* (go 1.13), skipping those lines missed 14 advisories (grpc 1.21.0,
|
|
56
|
+
* jwt-go, gorilla/websocket, ...) the build list carries.
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectedFromGoSum(content: string): Map<string, string>;
|
|
59
|
+
/** Semver precedence over Go versions; build metadata (`+incompatible`) is ignored. */
|
|
60
|
+
export declare function compareGoVersions(a: string, b: string): number;
|