@4da/mcp-server 6.2.4 → 6.2.5
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 +484 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +258 -9
- package/dist/live/fix-paths.js +790 -45
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +49 -0
- package/dist/live/version-compare.js +131 -0
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +59 -4
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- package/package.json +2 -2
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Which values may be spliced into a command an agent is told to run.
|
|
4
|
+
*
|
|
5
|
+
* Package names, installed versions, Cargo dependency keys and `[target.'cfg(...)']`
|
|
6
|
+
* headers come from the scanned project's own files: a cloned repository controls
|
|
7
|
+
* them. upgrade_planner put a Cargo.toml target header inside `--target "..."`,
|
|
8
|
+
* where bash and PowerShell still expand `$(...)` and backticks, and a dependency
|
|
9
|
+
* key unquoted after `--rename` (audit 6.2.5 C-1). A command is named only when
|
|
10
|
+
* every spliced value is a plain token no shell (sh, bash, zsh, cmd.exe,
|
|
11
|
+
* PowerShell) reads specially; otherwise the caller falls back to words.
|
|
12
|
+
*/
|
|
13
|
+
/** npm (scoped or not), crates.io and PyPI names: letters, digits, `.`, `_`, `-`; npm's `@scope/`. */
|
|
14
|
+
const PACKAGE_NAME = /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
|
+
/** Go module paths: host/path segments of letters, digits, `.`, `_`, `~`, `-`. */
|
|
16
|
+
const GO_MODULE = /^[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9._~-]+)*$/;
|
|
17
|
+
/** Versions as every supported ecosystem writes them: `1.2.3`, `v1.2.3`, `1.0.0-rc.1+build`, `2.0.0b1`. */
|
|
18
|
+
const VERSION = /^v?[0-9A-Za-z][0-9A-Za-z.+_-]*$/;
|
|
19
|
+
/** A Cargo target given as a plain triple (`x86_64-pc-windows-msvc`). */
|
|
20
|
+
const TARGET_TRIPLE = /^[A-Za-z0-9_.-]+$/;
|
|
21
|
+
/** A Cargo `cfg(...)` target with no string literals: `cfg(windows)`, `cfg(not(unix))`, `cfg(all(unix, target_pointer_width = 64))`. */
|
|
22
|
+
const CFG_PLAIN = /^cfg\([A-Za-z0-9_(), =]*\)$/;
|
|
23
|
+
export function isShellSafeName(name, ecosystem) {
|
|
24
|
+
return (ecosystem === "Go" ? GO_MODULE : PACKAGE_NAME).test(name) && name.length <= 214;
|
|
25
|
+
}
|
|
26
|
+
export function isShellSafeVersion(version) {
|
|
27
|
+
return VERSION.test(version) && version.length <= 128;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A Cargo target header that can go inside double quotes on every shell. A `cfg` with a string
|
|
31
|
+
* literal (`cfg(target_os = "linux")`) cannot: its quotes end the argument early. It is left to
|
|
32
|
+
* the words, which was always the only correct answer for it.
|
|
33
|
+
*/
|
|
34
|
+
export function isShellSafeCargoTarget(target) {
|
|
35
|
+
return (TARGET_TRIPLE.test(target) || CFG_PLAIN.test(target)) && target.length <= 256;
|
|
36
|
+
}
|
package/dist/live/types.d.ts
CHANGED
|
@@ -86,7 +86,8 @@ export interface OsvVulnerability {
|
|
|
86
86
|
score: string;
|
|
87
87
|
}>;
|
|
88
88
|
affected?: Array<{
|
|
89
|
-
|
|
89
|
+
/** Optional in the OSV schema: an entry with only GIT ranges has no package (EEF-CVE-2026-56812). */
|
|
90
|
+
package?: {
|
|
90
91
|
name: string;
|
|
91
92
|
ecosystem: string;
|
|
92
93
|
};
|
|
@@ -101,14 +102,20 @@ export interface OsvVulnerability {
|
|
|
101
102
|
}>;
|
|
102
103
|
/** Versions listed explicitly, in addition to (or instead of) the ranges. */
|
|
103
104
|
versions?: string[];
|
|
105
|
+
/** RustSec puts `informational` (unmaintained | unsound | notice) here, per package. */
|
|
106
|
+
database_specific?: {
|
|
107
|
+
informational?: string;
|
|
108
|
+
};
|
|
104
109
|
}>;
|
|
105
110
|
references?: Array<{
|
|
106
111
|
type: string;
|
|
107
112
|
url: string;
|
|
108
113
|
}>;
|
|
109
|
-
/** GitHub-advisory severity label: LOW | MODERATE | HIGH | CRITICAL. */
|
|
114
|
+
/** GitHub-advisory severity label: LOW | MODERATE | HIGH | CRITICAL; `cwe_ids` (CWE-506 = malicious code). */
|
|
110
115
|
database_specific?: {
|
|
111
116
|
severity?: string;
|
|
117
|
+
cwe_ids?: string[];
|
|
118
|
+
informational?: string;
|
|
112
119
|
};
|
|
113
120
|
/** Set (to an RFC3339 timestamp) when the advisory has been withdrawn/retracted. */
|
|
114
121
|
withdrawn?: string;
|
|
@@ -126,6 +133,22 @@ export interface VulnerabilityEntry {
|
|
|
126
133
|
aliases: string[];
|
|
127
134
|
severity: "critical" | "high" | "medium" | "low" | "unknown";
|
|
128
135
|
cvssScore: number | null;
|
|
136
|
+
/**
|
|
137
|
+
* Set when `severity` is not this record's own: it rates nothing (Go's
|
|
138
|
+
* records) and names a GHSA alias, whose rating it took (osv-scanner.ts
|
|
139
|
+
* rateFromAliases). The alias's id.
|
|
140
|
+
*/
|
|
141
|
+
severityFrom?: string;
|
|
142
|
+
/** A malicious-package record (malware.ts isMalware: a MAL- id or alias, or CWE-506). */
|
|
143
|
+
malware?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Whether the record makes this a maintenance notice for this package
|
|
146
|
+
* (maintenance.ts isMaintenanceRecord: a stated `informational` class decides, the
|
|
147
|
+
* wording only when none is stated). Absent only on foreign or stub entries, which
|
|
148
|
+
* fall back to the summary wording; the scan cache key moved to osv:v3 so entries
|
|
149
|
+
* cached by 6.2.4 without it are never read.
|
|
150
|
+
*/
|
|
151
|
+
maintenance?: boolean;
|
|
129
152
|
summary: string;
|
|
130
153
|
fixedVersion: string | null;
|
|
131
154
|
published: string;
|
|
@@ -164,10 +187,55 @@ export interface VulnerabilityScanResult {
|
|
|
164
187
|
unknown: number;
|
|
165
188
|
};
|
|
166
189
|
vulnerabilities: VulnerabilityEntry[];
|
|
190
|
+
/** Dependencies OSV answered for and found no advisory on (unchecked ones are never counted clean). */
|
|
167
191
|
cleanCount: number;
|
|
168
192
|
scanDurationMs: number;
|
|
169
193
|
cached: boolean;
|
|
194
|
+
/** True when any dependency has no advisory answer (`unchecked` > 0), or lookups are disabled. */
|
|
170
195
|
offline: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Dependencies with NO advisory answer: OSV.dev could not be reached, failed,
|
|
198
|
+
* timed out, or the query budget ran out, and no earlier answer was cached.
|
|
199
|
+
* Their absence from `vulnerabilities` means nothing. Absent (= 0) on older
|
|
200
|
+
* stubs.
|
|
201
|
+
*/
|
|
202
|
+
unchecked?: number;
|
|
203
|
+
/** Why `unchecked` dependencies have no answer, in words; null when every one was answered. */
|
|
204
|
+
uncheckedReason?: string | null;
|
|
205
|
+
/** Dependencies answered from an OSV answer older than the 1 h cache life, because a fresh query failed. */
|
|
206
|
+
staleAnswers?: number;
|
|
207
|
+
/** Why fresh answers were not had for the `staleAnswers` dependencies. */
|
|
208
|
+
staleReason?: string;
|
|
209
|
+
/** fetched_at (SQLite UTC, "YYYY-MM-DD HH:MM:SS") of the oldest stale answer used. */
|
|
210
|
+
staleOldestFetchedAt?: string | null;
|
|
211
|
+
/** Records rated from a GHSA alias, and those that name one but stay unknown (cap, time budget, no answer), with why. */
|
|
212
|
+
aliasSeverity?: AliasSeverityReport;
|
|
213
|
+
}
|
|
214
|
+
export interface AliasSeverityReport {
|
|
215
|
+
/** Records whose severity came from their GHSA alias. */
|
|
216
|
+
rated: number;
|
|
217
|
+
/** GHSA alias ids that could not be read (cap, time budget, no answer): they never lower a grade. */
|
|
218
|
+
unresolved: string[];
|
|
219
|
+
/** Record ids that name a GHSA alias and are still unknown: no alias of theirs could be read or graded. */
|
|
220
|
+
ungraded: string[];
|
|
221
|
+
/** Why `unresolved` or `ungraded` is not empty; null when both are. */
|
|
222
|
+
reason: string | null;
|
|
223
|
+
}
|
|
224
|
+
/** How much of a dependency set OSV actually answered for. */
|
|
225
|
+
export interface VulnerabilityCoverage {
|
|
226
|
+
complete: false;
|
|
227
|
+
/** Dependencies sent for checking. */
|
|
228
|
+
total_dependencies: number;
|
|
229
|
+
/** Dependencies with an advisory answer (fresh or stale). */
|
|
230
|
+
checked_dependencies: number;
|
|
231
|
+
/** Dependencies with NO answer: nothing is known about their advisories. */
|
|
232
|
+
unchecked_dependencies: number;
|
|
233
|
+
/** Dependencies answered only from an expired cached answer (a fresh query failed); present when any were. */
|
|
234
|
+
stale_dependencies?: number;
|
|
235
|
+
/** Why, in words ("OSV.dev unreachable (fetch failed)", "live lookups disabled (FOURDA_OFFLINE=true)"). */
|
|
236
|
+
reason: string;
|
|
237
|
+
/** One line to read first; never a clean result. */
|
|
238
|
+
note: string;
|
|
171
239
|
}
|
|
172
240
|
export interface LiveHeadline {
|
|
173
241
|
id: string;
|
|
@@ -213,6 +281,14 @@ export interface RegistryPackageInfo {
|
|
|
213
281
|
* workspaces are listed comma-separated.
|
|
214
282
|
*/
|
|
215
283
|
installedVersion?: string;
|
|
284
|
+
/**
|
|
285
|
+
* dependency_health: present when `latestStableVersion` is NOT offered as an upgrade: an advisory
|
|
286
|
+
* affects it or the registry withdrew it. The newest release above `currentVersion` that is
|
|
287
|
+
* neither (null when none), which `versionsBehind` is then measured to.
|
|
288
|
+
*/
|
|
289
|
+
latestSafeVersion?: string | null;
|
|
290
|
+
/** dependency_health: why `latestStableVersion` is not offered, or that it could not be checked. */
|
|
291
|
+
latestStableNote?: string;
|
|
216
292
|
}
|
|
217
293
|
export interface SemverDistance {
|
|
218
294
|
major: number;
|
|
@@ -224,7 +300,23 @@ export interface DependencyHealthResult {
|
|
|
224
300
|
scannedAt: string;
|
|
225
301
|
projectPath: string;
|
|
226
302
|
ecosystemsScanned: string[];
|
|
303
|
+
/**
|
|
304
|
+
* Direct dependencies checked against their registries (the rows in
|
|
305
|
+
* `dependencies`; devDependencies only with include_dev). Freshness,
|
|
306
|
+
* deprecation and `outdatedCount` / `deprecatedCount` are over these.
|
|
307
|
+
*/
|
|
227
308
|
totalDeps: number;
|
|
309
|
+
/** Installed packages (direct and transitive) the vulnerability scan covered: what `vulnerableCount` ranges over. */
|
|
310
|
+
treeDeps?: number;
|
|
311
|
+
/** Of `vulnerableCount`, the packages a manifest declares directly. Null when the check is incomplete. */
|
|
312
|
+
vulnerableDirectCount?: number | null;
|
|
313
|
+
/** The penalties the score subtracts from 100, and how each is computed. Absent when the score is withheld. */
|
|
314
|
+
healthScoreBasis?: {
|
|
315
|
+
vulnerabilities: number;
|
|
316
|
+
outdated: number;
|
|
317
|
+
deprecated: number;
|
|
318
|
+
note: string;
|
|
319
|
+
};
|
|
228
320
|
outdatedCount: number;
|
|
229
321
|
deprecatedCount: number;
|
|
230
322
|
/**
|
|
@@ -232,21 +324,55 @@ export interface DependencyHealthResult {
|
|
|
232
324
|
* maintenance notice — the same set `vulnerability_scan` reports as
|
|
233
325
|
* `total_vulnerable_packages`, so the two tools agree over one scan.
|
|
234
326
|
*/
|
|
235
|
-
vulnerableCount: number;
|
|
236
327
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
328
|
+
* Distinct vulnerable packages across the full installed tree (`treeDeps`),
|
|
329
|
+
* the same set vulnerability_scan reports as `total_vulnerable_packages`
|
|
330
|
+
* for the same scope. It can exceed `totalDeps`, which counts direct
|
|
331
|
+
* dependencies only. Null when any dependency could not be checked.
|
|
332
|
+
*/
|
|
333
|
+
vulnerableCount: number | null;
|
|
334
|
+
/**
|
|
335
|
+
* Distinct advisories the scan returned (records naming each other, such as
|
|
336
|
+
* a GHSA and its PYSEC twin, are one advisory per installed copy, as in
|
|
337
|
+
* vulnerability_scan), before the platform and maintenance splits. Present
|
|
338
|
+
* so nothing is hidden by the `vulnerableCount` filter: the difference is
|
|
339
|
+
* platform-inactive packages and unmaintained-dependency notices.
|
|
241
340
|
*/
|
|
242
341
|
advisoryCount: number;
|
|
243
|
-
|
|
342
|
+
/** Null when vulnerability data is missing or incomplete: a score computed without it would read as healthier than it is. */
|
|
343
|
+
healthScore: number | null;
|
|
344
|
+
/**
|
|
345
|
+
* Why `healthScore` is null: OSV gave no answer for some dependencies, a
|
|
346
|
+
* lockfile could not be read, or nothing was resolved at all. Absent when
|
|
347
|
+
* there is a score.
|
|
348
|
+
*/
|
|
349
|
+
withheldReason?: "vulnerability_data_incomplete" | "lockfile_unreadable" | "nothing_checked";
|
|
350
|
+
/**
|
|
351
|
+
* Present when the score is over the checked part only: a lockfile read with
|
|
352
|
+
* entries skipped (PARTIAL) or a requirements include refused (REFUSED).
|
|
353
|
+
* The counts are lower bounds; `notes` say what is missing.
|
|
354
|
+
*/
|
|
355
|
+
scoreCoverage?: {
|
|
356
|
+
complete: false;
|
|
357
|
+
basis: "checked_part";
|
|
358
|
+
notes: string[];
|
|
359
|
+
};
|
|
360
|
+
/** Records graded from a GHSA alias, and those still unknown, as in vulnerability_scan's `_meta.alias_severity`. */
|
|
361
|
+
aliasSeverity?: AliasSeverityReport;
|
|
362
|
+
/** Present when the vulnerability check is incomplete: what was not checked and why. */
|
|
363
|
+
vulnerabilityCoverage?: VulnerabilityCoverage;
|
|
244
364
|
dependencies: RegistryPackageInfo[];
|
|
365
|
+
/**
|
|
366
|
+
* Advisories (not packages) on the full tree by their own grade, over the same actionable set: the
|
|
367
|
+
* same counts as vulnerability_scan's `advisory_by_severity` for the same scope. `unknown` counts the
|
|
368
|
+
* ungraded ones, which were left out of every bucket before (pygoat: 186 counted of 187).
|
|
369
|
+
*/
|
|
245
370
|
vulnerabilitySummary: {
|
|
246
371
|
critical: number;
|
|
247
372
|
high: number;
|
|
248
373
|
medium: number;
|
|
249
374
|
low: number;
|
|
375
|
+
unknown: number;
|
|
250
376
|
} | null;
|
|
251
377
|
summary: string;
|
|
252
378
|
scanDurationMs: number;
|
|
@@ -25,13 +25,62 @@ interface Pep440 {
|
|
|
25
25
|
}
|
|
26
26
|
export declare function parsePep440(version: string): Pep440 | null;
|
|
27
27
|
export declare function comparePep440(a: Pep440, b: Pep440): number;
|
|
28
|
+
/**
|
|
29
|
+
* A Composer version as `VersionParser::normalize` reads it: an optional `v`,
|
|
30
|
+
* up to four (here: any number of) numeric parts padded with zeros, then a
|
|
31
|
+
* stability suffix. OSV's Packagist ranges use two-part bounds
|
|
32
|
+
* (`introduced: "6.4"`, symfony/process GHSA-r39x-jcww-82v6), which
|
|
33
|
+
* MAJOR.MINOR.PATCH could not read: compared as "equal" to everything, the
|
|
34
|
+
* 5.4 line's fix 5.4.51 counted as inside [6.4, 6.4.33) and the target jumped
|
|
35
|
+
* to 8.0.5 (readiness audit H2, 2026-10-16). `1.2` is `1.2.0.0`.
|
|
36
|
+
*/
|
|
37
|
+
interface ComposerVersion {
|
|
38
|
+
release: number[];
|
|
39
|
+
/** dev=0 < alpha=1 < beta=2 < RC=3 < stable=4 < patch=5. */
|
|
40
|
+
stability: number;
|
|
41
|
+
stabilityNum: number[];
|
|
42
|
+
/** A trailing `-dev` (`1.0.0-beta2-dev`): below the same version without it. */
|
|
43
|
+
dev: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function parseComposerVersion(version: string): ComposerVersion | null;
|
|
46
|
+
export declare function compareComposer(a: ComposerVersion, b: ComposerVersion): number;
|
|
47
|
+
/**
|
|
48
|
+
* Gem::Version's canonical segments: `-` reads as `.pre.`, the text splits
|
|
49
|
+
* into runs of digits and letters, and trailing zeros of the release part
|
|
50
|
+
* and of the prerelease part are dropped, so `3.0` equals `3.0.0` (OSV's
|
|
51
|
+
* RubyGems bounds are two-part: rack GHSA-6xw4-3v39-52mm `introduced: "3.0"`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseGemVersion(version: string): Array<number | string> | null;
|
|
54
|
+
/** Gem::Version#<=>: a missing segment is 0, a letter run sorts below a number. */
|
|
55
|
+
export declare function compareGem(a: Array<number | string>, b: Array<number | string>): number;
|
|
56
|
+
/**
|
|
57
|
+
* A version as the ecosystem itself writes it, for output. Go module versions always carry a `v`
|
|
58
|
+
* (go.mod, `go get`), while OSV's Go ranges and the proxy list read here do not: targetVersion
|
|
59
|
+
* 0.60.0 sat beside currentVersion v0.60.0's line (final release audit of 6.2.5, 7e). Every other
|
|
60
|
+
* ecosystem is returned as it is.
|
|
61
|
+
*/
|
|
62
|
+
export declare function inEcosystemForm<T extends string | null | undefined>(ecosystem: string, version: T): T;
|
|
28
63
|
/** Whether `version` is readable for this ecosystem's ordering. */
|
|
29
64
|
export declare function isComparable(version: string, ecosystem: OsvEcosystem | string): boolean;
|
|
65
|
+
/** Whether a readable version is a prerelease in Composer's or Gem::Version's own terms; null for other ecosystems or unreadable text. */
|
|
66
|
+
export declare function isComposerOrGemPrerelease(version: string, ecosystem: OsvEcosystem | string): boolean | null;
|
|
30
67
|
/**
|
|
31
68
|
* Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
|
|
32
69
|
* (the same contract as compareSemver, so an unreadable bound never decides).
|
|
33
70
|
*/
|
|
34
71
|
export declare function compareVersions(a: string, b: string, ecosystem: OsvEcosystem | string): number;
|
|
72
|
+
/**
|
|
73
|
+
* `version` against an advisory window's LOWER bound (`introduced`). As
|
|
74
|
+
* Composer reads a constraint, a Packagist bound with no stability suffix
|
|
75
|
+
* opens at that release's dev stability (`>=8.0` is `>= 8.0.0.0-dev`), so the
|
|
76
|
+
* line's own prereleases are inside it: GHSA-r39x-jcww-82v6 opens at `8.0`
|
|
77
|
+
* and lists v8.0.0-BETA1 and v8.0.0-RC1 as affected. Compared as a plain
|
|
78
|
+
* version they fell below it (review of 6.2.5 H2, 2026-10-18). The upper
|
|
79
|
+
* bound keeps the plain order on purpose: a prerelease of the fix release
|
|
80
|
+
* (8.0.5-RC1) stays affected, the safe reading (Composer would exclude it).
|
|
81
|
+
* Every other ecosystem: compareVersions.
|
|
82
|
+
*/
|
|
83
|
+
export declare function compareToLowerBound(version: string, bound: string, ecosystem: OsvEcosystem | string): number;
|
|
35
84
|
/** Highest of `versions` in this ecosystem's order, or null when empty. */
|
|
36
85
|
export declare function maxVersion(versions: string[], ecosystem: OsvEcosystem | string): string | null;
|
|
37
86
|
/** Package-name equality as the ecosystem's registry defines it. */
|
|
@@ -77,10 +77,95 @@ function compareLocal(a, b) {
|
|
|
77
77
|
}
|
|
78
78
|
return Math.sign(a.length - b.length);
|
|
79
79
|
}
|
|
80
|
+
const COMPOSER = /^v?(\d+(?:\.\d+){0,3})(?:[._-]?(stable|beta|b|alpha|a|rc|patch|pl|p)((?:[.-]?\d+)*))?(?:[.-]?(dev))?(?:\+[0-9a-z.-]+)?$/i;
|
|
81
|
+
const COMPOSER_RANK = { alpha: 1, a: 1, beta: 2, b: 2, rc: 3, stable: 4, patch: 5, pl: 5, p: 5 };
|
|
82
|
+
export function parseComposerVersion(version) {
|
|
83
|
+
const m = COMPOSER.exec(version.trim());
|
|
84
|
+
if (!m)
|
|
85
|
+
return null;
|
|
86
|
+
const token = m[2]?.toLowerCase();
|
|
87
|
+
const devOnly = !token && m[4] !== undefined;
|
|
88
|
+
return {
|
|
89
|
+
release: m[1].split(".").map(Number),
|
|
90
|
+
stability: devOnly ? 0 : token ? COMPOSER_RANK[token] : 4,
|
|
91
|
+
stabilityNum: m[3] ? m[3].split(/[.-]/).filter(Boolean).map(Number) : [],
|
|
92
|
+
dev: !devOnly && m[4] !== undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function comparePadded(a, b) {
|
|
96
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
97
|
+
const x = a[i] ?? 0;
|
|
98
|
+
const y = b[i] ?? 0;
|
|
99
|
+
if (x !== y)
|
|
100
|
+
return x > y ? 1 : -1;
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
export function compareComposer(a, b) {
|
|
105
|
+
const byRelease = comparePadded(a.release, b.release) || Math.sign(a.stability - b.stability);
|
|
106
|
+
if (byRelease !== 0)
|
|
107
|
+
return byRelease;
|
|
108
|
+
// A bare tag sorts below a numbered one: `beta` < `beta0` (composer/semver, by version_compare).
|
|
109
|
+
const bareA = a.stabilityNum.length === 0;
|
|
110
|
+
const bareB = b.stabilityNum.length === 0;
|
|
111
|
+
if (bareA !== bareB)
|
|
112
|
+
return bareA ? -1 : 1;
|
|
113
|
+
return comparePadded(a.stabilityNum, b.stabilityNum) || (a.dev === b.dev ? 0 : a.dev ? -1 : 1);
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Gem::Version (RubyGems)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/** Gem::Version::ANCHORED_VERSION_PATTERN. */
|
|
119
|
+
const GEM = /^[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
120
|
+
/**
|
|
121
|
+
* Gem::Version's canonical segments: `-` reads as `.pre.`, the text splits
|
|
122
|
+
* into runs of digits and letters, and trailing zeros of the release part
|
|
123
|
+
* and of the prerelease part are dropped, so `3.0` equals `3.0.0` (OSV's
|
|
124
|
+
* RubyGems bounds are two-part: rack GHSA-6xw4-3v39-52mm `introduced: "3.0"`).
|
|
125
|
+
*/
|
|
126
|
+
export function parseGemVersion(version) {
|
|
127
|
+
const v = version.trim();
|
|
128
|
+
if (!GEM.test(v))
|
|
129
|
+
return null;
|
|
130
|
+
const segments = (v.replace(/-/g, ".pre.").match(/[0-9]+|[a-z]+/gi) ?? []).map((s) => (/^\d/.test(s) ? Number(s) : s));
|
|
131
|
+
const firstString = segments.findIndex((s) => typeof s === "string");
|
|
132
|
+
const cut = firstString < 0 ? segments.length : firstString;
|
|
133
|
+
const trim = (part) => {
|
|
134
|
+
let end = part.length;
|
|
135
|
+
while (end > 0 && part[end - 1] === 0)
|
|
136
|
+
end--;
|
|
137
|
+
return part.slice(0, end);
|
|
138
|
+
};
|
|
139
|
+
return [...trim(segments.slice(0, cut)), ...trim(segments.slice(cut))];
|
|
140
|
+
}
|
|
141
|
+
/** Gem::Version#<=>: a missing segment is 0, a letter run sorts below a number. */
|
|
142
|
+
export function compareGem(a, b) {
|
|
143
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
144
|
+
const x = a[i] ?? 0;
|
|
145
|
+
const y = b[i] ?? 0;
|
|
146
|
+
if (x === y)
|
|
147
|
+
continue;
|
|
148
|
+
if (typeof x !== typeof y)
|
|
149
|
+
return typeof x === "string" ? -1 : 1;
|
|
150
|
+
return x > y ? 1 : -1;
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
80
154
|
// ---------------------------------------------------------------------------
|
|
81
155
|
// Dispatch
|
|
82
156
|
// ---------------------------------------------------------------------------
|
|
83
157
|
const stripGoV = (v) => v.replace(/^v(?=\d)/, "");
|
|
158
|
+
/**
|
|
159
|
+
* A version as the ecosystem itself writes it, for output. Go module versions always carry a `v`
|
|
160
|
+
* (go.mod, `go get`), while OSV's Go ranges and the proxy list read here do not: targetVersion
|
|
161
|
+
* 0.60.0 sat beside currentVersion v0.60.0's line (final release audit of 6.2.5, 7e). Every other
|
|
162
|
+
* ecosystem is returned as it is.
|
|
163
|
+
*/
|
|
164
|
+
export function inEcosystemForm(ecosystem, version) {
|
|
165
|
+
if (ecosystem !== "Go" || typeof version !== "string" || !/^\d/.test(version))
|
|
166
|
+
return version;
|
|
167
|
+
return `v${version}`;
|
|
168
|
+
}
|
|
84
169
|
/** Whether `version` is readable for this ecosystem's ordering. */
|
|
85
170
|
export function isComparable(version, ecosystem) {
|
|
86
171
|
if (version === "0")
|
|
@@ -93,10 +178,26 @@ export function isComparable(version, ecosystem) {
|
|
|
93
178
|
case "npm":
|
|
94
179
|
case "crates.io":
|
|
95
180
|
return parseSemverPrecedence(version) !== null;
|
|
181
|
+
case "Packagist":
|
|
182
|
+
return parseComposerVersion(version) !== null;
|
|
183
|
+
case "RubyGems":
|
|
184
|
+
return parseGemVersion(version) !== null;
|
|
96
185
|
default:
|
|
97
186
|
return parseSemver(version) !== null;
|
|
98
187
|
}
|
|
99
188
|
}
|
|
189
|
+
/** Whether a readable version is a prerelease in Composer's or Gem::Version's own terms; null for other ecosystems or unreadable text. */
|
|
190
|
+
export function isComposerOrGemPrerelease(version, ecosystem) {
|
|
191
|
+
if (ecosystem === "Packagist") {
|
|
192
|
+
const c = parseComposerVersion(version);
|
|
193
|
+
return c ? c.stability < 4 || c.dev : null;
|
|
194
|
+
}
|
|
195
|
+
if (ecosystem === "RubyGems") {
|
|
196
|
+
const g = parseGemVersion(version);
|
|
197
|
+
return g ? g.some((s) => typeof s === "string") : null;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
100
201
|
/**
|
|
101
202
|
* Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
|
|
102
203
|
* (the same contract as compareSemver, so an unreadable bound never decides).
|
|
@@ -119,10 +220,40 @@ export function compareVersions(a, b, ecosystem) {
|
|
|
119
220
|
case "npm":
|
|
120
221
|
case "crates.io":
|
|
121
222
|
return compareVersionPrecedence(a, b) ?? compareSemver(a, b);
|
|
223
|
+
case "Packagist": {
|
|
224
|
+
const ca = parseComposerVersion(a);
|
|
225
|
+
const cb = parseComposerVersion(b);
|
|
226
|
+
return ca && cb ? compareComposer(ca, cb) : 0;
|
|
227
|
+
}
|
|
228
|
+
case "RubyGems": {
|
|
229
|
+
const ga = parseGemVersion(a);
|
|
230
|
+
const gb = parseGemVersion(b);
|
|
231
|
+
return ga && gb ? compareGem(ga, gb) : 0;
|
|
232
|
+
}
|
|
122
233
|
default:
|
|
123
234
|
return compareSemver(a, b);
|
|
124
235
|
}
|
|
125
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* `version` against an advisory window's LOWER bound (`introduced`). As
|
|
239
|
+
* Composer reads a constraint, a Packagist bound with no stability suffix
|
|
240
|
+
* opens at that release's dev stability (`>=8.0` is `>= 8.0.0.0-dev`), so the
|
|
241
|
+
* line's own prereleases are inside it: GHSA-r39x-jcww-82v6 opens at `8.0`
|
|
242
|
+
* and lists v8.0.0-BETA1 and v8.0.0-RC1 as affected. Compared as a plain
|
|
243
|
+
* version they fell below it (review of 6.2.5 H2, 2026-10-18). The upper
|
|
244
|
+
* bound keeps the plain order on purpose: a prerelease of the fix release
|
|
245
|
+
* (8.0.5-RC1) stays affected, the safe reading (Composer would exclude it).
|
|
246
|
+
* Every other ecosystem: compareVersions.
|
|
247
|
+
*/
|
|
248
|
+
export function compareToLowerBound(version, bound, ecosystem) {
|
|
249
|
+
if (ecosystem === "Packagist" && bound !== "0") {
|
|
250
|
+
const v = parseComposerVersion(version);
|
|
251
|
+
const b = parseComposerVersion(bound);
|
|
252
|
+
if (v && b && b.stability === 4 && !/stable/i.test(bound))
|
|
253
|
+
return compareComposer(v, { ...b, stability: 0, stabilityNum: [], dev: false });
|
|
254
|
+
}
|
|
255
|
+
return compareVersions(version, bound, ecosystem);
|
|
256
|
+
}
|
|
126
257
|
/** Highest of `versions` in this ecosystem's order, or null when empty. */
|
|
127
258
|
export function maxVersion(versions, ecosystem) {
|
|
128
259
|
if (versions.length === 0)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which releases a registry has withdrawn, so that no fix target is ever one
|
|
3
|
+
* of them (4DA FX-H5, the app's `osv/release_facts.rs`; the rule the shared
|
|
4
|
+
* fix_target vectors pin, README "Optional fix_target registry fields").
|
|
5
|
+
*
|
|
6
|
+
* - crates.io: `yanked`.
|
|
7
|
+
* - PyPI: a release whose every file is `yanked` (PEP 592).
|
|
8
|
+
* - npm: `deprecated`, but ONLY when a later, non-deprecated, stable release
|
|
9
|
+
* exists on the same caret line (same major; same minor for 0.x). npm's
|
|
10
|
+
* `deprecated` is often a whole-major "please upgrade" notice rather than a
|
|
11
|
+
* withdrawn release: glob deprecates 160 of its 168 versions ("Old versions
|
|
12
|
+
* of glob are not supported"), 10.5.0 included, and excluding them all sent
|
|
13
|
+
* glob 10.4.5 to 12.0.0, a major jump. lodash 4.18.0 ("Bad release") is
|
|
14
|
+
* excluded because 4.18.1 is clean on 4.x. A package whose `latest` is
|
|
15
|
+
* itself deprecated (retired whole) excludes nothing.
|
|
16
|
+
*
|
|
17
|
+
* Pure functions; the registry readers (fix-paths.ts, npm-packument.ts) call
|
|
18
|
+
* them on what they already fetched.
|
|
19
|
+
*/
|
|
20
|
+
import type { PackageReleaseIndex } from "./release-metadata.js";
|
|
21
|
+
/**
|
|
22
|
+
* dependency_check's view: every release of `index` that is never a candidate, with the
|
|
23
|
+
* registry's words. Withdrawn records (crates.io/PyPI yanked, npm unpublished, Go retracted:
|
|
24
|
+
* none of them installable as asked) plus the npm deprecations the rule above withdraws.
|
|
25
|
+
*/
|
|
26
|
+
export declare function excludedReleases(index: PackageReleaseIndex): Map<string, string>;
|
|
27
|
+
/** npm's per-version `deprecated`: a non-empty message, or `true` (the app's `npm_deprecated`). */
|
|
28
|
+
export declare function isNpmDeprecated(value: unknown): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The npm releases to exclude as fix targets, from a packument's `versions`
|
|
31
|
+
* (version -> its `deprecated`) and `dist-tags.latest` (see the module notes).
|
|
32
|
+
*/
|
|
33
|
+
export declare function npmExcludedVersions(deprecation: Record<string, unknown>, latest: string | null | undefined): string[];
|
|
34
|
+
/** How a registry names a withdrawal, for the release `version`. */
|
|
35
|
+
export declare function withdrawalText(ecosystem: string, version: string, detail?: string | null): string;
|
|
36
|
+
/**
|
|
37
|
+
* The withdrawn releases in plain words, releases with the same reason said once:
|
|
38
|
+
* "7.5.3, 7.5.4 and 7.5.5 are deprecated on npm ("Old versions of tar ...")".
|
|
39
|
+
*/
|
|
40
|
+
export declare function sayWithdrawn(versions: string[], why: Record<string, string>, ecosystem: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* The plain-words note for a target moved past withdrawn releases, or for no
|
|
43
|
+
* target because every release that would fix it was withdrawn (the app's
|
|
44
|
+
* Upgrade Plan wording: "No installable fix").
|
|
45
|
+
* "4.18.0 is deprecated on npm ("Bad release..."); 4.18.1 is the smallest clean release"
|
|
46
|
+
* "No installable fix: 0.3.17 is yanked on crates.io, and no later release is known to clear it"
|
|
47
|
+
* `why` holds the registry's own text per withdrawn release (withdrawalText).
|
|
48
|
+
* `clean: false`: the target clears only the advisories a release fixes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function withheldNote(withheld: string[], target: string | null, why: Record<string, string>, ecosystem: string, clean?: boolean): string | null;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Which releases a registry has withdrawn, so that no fix target is ever one
|
|
4
|
+
* of them (4DA FX-H5, the app's `osv/release_facts.rs`; the rule the shared
|
|
5
|
+
* fix_target vectors pin, README "Optional fix_target registry fields").
|
|
6
|
+
*
|
|
7
|
+
* - crates.io: `yanked`.
|
|
8
|
+
* - PyPI: a release whose every file is `yanked` (PEP 592).
|
|
9
|
+
* - npm: `deprecated`, but ONLY when a later, non-deprecated, stable release
|
|
10
|
+
* exists on the same caret line (same major; same minor for 0.x). npm's
|
|
11
|
+
* `deprecated` is often a whole-major "please upgrade" notice rather than a
|
|
12
|
+
* withdrawn release: glob deprecates 160 of its 168 versions ("Old versions
|
|
13
|
+
* of glob are not supported"), 10.5.0 included, and excluding them all sent
|
|
14
|
+
* glob 10.4.5 to 12.0.0, a major jump. lodash 4.18.0 ("Bad release") is
|
|
15
|
+
* excluded because 4.18.1 is clean on 4.x. A package whose `latest` is
|
|
16
|
+
* itself deprecated (retired whole) excludes nothing.
|
|
17
|
+
*
|
|
18
|
+
* Pure functions; the registry readers (fix-paths.ts, npm-packument.ts) call
|
|
19
|
+
* them on what they already fetched.
|
|
20
|
+
*/
|
|
21
|
+
import { comparePrecedence, parseSemverPrecedence } from "./semver-precedence.js";
|
|
22
|
+
/**
|
|
23
|
+
* dependency_check's view: every release of `index` that is never a candidate, with the
|
|
24
|
+
* registry's words. Withdrawn records (crates.io/PyPI yanked, npm unpublished, Go retracted:
|
|
25
|
+
* none of them installable as asked) plus the npm deprecations the rule above withdraws.
|
|
26
|
+
*/
|
|
27
|
+
export function excludedReleases(index) {
|
|
28
|
+
const out = new Map();
|
|
29
|
+
const releases = Object.values(index.releases);
|
|
30
|
+
for (const r of releases) {
|
|
31
|
+
if (r.withdrawn)
|
|
32
|
+
out.set(r.version, r.withdrawn === "yanked" ? withdrawalText(index.ecosystem, r.version) : `${r.version} was ${r.withdrawn} from the registry`);
|
|
33
|
+
}
|
|
34
|
+
if (index.ecosystem === "npm") {
|
|
35
|
+
const live = Object.fromEntries(releases.filter((r) => !r.withdrawn).map((r) => [r.version, r.deprecated]));
|
|
36
|
+
for (const v of npmExcludedVersions(live, index.latest))
|
|
37
|
+
out.set(v, withdrawalText("npm", v, index.releases[v]?.deprecated));
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** npm's per-version `deprecated`: a non-empty message, or `true` (the app's `npm_deprecated`). */
|
|
42
|
+
export function isNpmDeprecated(value) {
|
|
43
|
+
return (typeof value === "string" && value !== "") || value === true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The npm releases to exclude as fix targets, from a packument's `versions`
|
|
47
|
+
* (version -> its `deprecated`) and `dist-tags.latest` (see the module notes).
|
|
48
|
+
*/
|
|
49
|
+
export function npmExcludedVersions(deprecation, latest) {
|
|
50
|
+
if (latest && latest in deprecation && isNpmDeprecated(deprecation[latest]))
|
|
51
|
+
return [];
|
|
52
|
+
const clean = Object.entries(deprecation)
|
|
53
|
+
.filter(([, d]) => !isNpmDeprecated(d))
|
|
54
|
+
.map(([v]) => parseSemverPrecedence(v))
|
|
55
|
+
.filter((p) => p !== null && p.prerelease.length === 0);
|
|
56
|
+
return Object.entries(deprecation)
|
|
57
|
+
.filter(([, d]) => isNpmDeprecated(d))
|
|
58
|
+
.filter(([v]) => {
|
|
59
|
+
const p = parseSemverPrecedence(v);
|
|
60
|
+
return p !== null && clean.some((c) => c.major === p.major && (p.major > 0 || c.minor === p.minor) && comparePrecedence(c, p) > 0);
|
|
61
|
+
})
|
|
62
|
+
.map(([v]) => v);
|
|
63
|
+
}
|
|
64
|
+
/** How a registry names a withdrawal, for the release `version`. */
|
|
65
|
+
export function withdrawalText(ecosystem, version, detail) {
|
|
66
|
+
const quoted = detail?.trim() ? ` ("${clip(detail.trim(), 120)}")` : "";
|
|
67
|
+
switch (ecosystem) {
|
|
68
|
+
case "npm":
|
|
69
|
+
return `${version} is deprecated on npm${quoted}`;
|
|
70
|
+
case "crates.io":
|
|
71
|
+
return `${version} is yanked on crates.io`;
|
|
72
|
+
case "PyPI":
|
|
73
|
+
return `${version} is yanked on PyPI${quoted}`;
|
|
74
|
+
default:
|
|
75
|
+
return `${version} is withdrawn on the registry${quoted}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** At most `max` characters, cut at a word, "..." when cut. */
|
|
79
|
+
function clip(text, max) {
|
|
80
|
+
if (text.length <= max)
|
|
81
|
+
return text;
|
|
82
|
+
const cut = text.slice(0, max);
|
|
83
|
+
return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), max / 2)).replace(/[\s,;:.]+$/, "")}...`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The withdrawn releases in plain words, releases with the same reason said once:
|
|
87
|
+
* "7.5.3, 7.5.4 and 7.5.5 are deprecated on npm ("Old versions of tar ...")".
|
|
88
|
+
*/
|
|
89
|
+
export function sayWithdrawn(versions, why, ecosystem) {
|
|
90
|
+
const groups = [];
|
|
91
|
+
for (const v of versions) {
|
|
92
|
+
const text = why[v] ?? withdrawalText(ecosystem, v);
|
|
93
|
+
const rest = text.startsWith(`${v} is `) ? text.slice(v.length + " is ".length) : null;
|
|
94
|
+
const last = groups[groups.length - 1];
|
|
95
|
+
if (rest !== null && last && last.rest === rest)
|
|
96
|
+
last.versions.push(v);
|
|
97
|
+
else
|
|
98
|
+
groups.push({ rest: rest ?? text, versions: rest === null ? [] : [v] });
|
|
99
|
+
}
|
|
100
|
+
return groups
|
|
101
|
+
.map((g) => {
|
|
102
|
+
if (g.versions.length === 0)
|
|
103
|
+
return g.rest;
|
|
104
|
+
const names = g.versions.length === 1 ? g.versions[0] : `${g.versions.slice(0, -1).join(", ")} and ${g.versions[g.versions.length - 1]}`;
|
|
105
|
+
return `${names} ${g.versions.length === 1 ? "is" : "are"} ${g.rest}`;
|
|
106
|
+
})
|
|
107
|
+
.join("; ");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The plain-words note for a target moved past withdrawn releases, or for no
|
|
111
|
+
* target because every release that would fix it was withdrawn (the app's
|
|
112
|
+
* Upgrade Plan wording: "No installable fix").
|
|
113
|
+
* "4.18.0 is deprecated on npm ("Bad release..."); 4.18.1 is the smallest clean release"
|
|
114
|
+
* "No installable fix: 0.3.17 is yanked on crates.io, and no later release is known to clear it"
|
|
115
|
+
* `why` holds the registry's own text per withdrawn release (withdrawalText).
|
|
116
|
+
* `clean: false`: the target clears only the advisories a release fixes.
|
|
117
|
+
*/
|
|
118
|
+
export function withheldNote(withheld, target, why, ecosystem, clean = true) {
|
|
119
|
+
if (withheld.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
const said = sayWithdrawn(withheld, why, ecosystem);
|
|
122
|
+
if (!target)
|
|
123
|
+
return `No installable fix: ${said}, and no later release is known to clear it — pin, patch, or replace`;
|
|
124
|
+
return `${said}; ${target} is the smallest ${clean ? "clean release" : "installable release that clears the advisories a release fixes"}`;
|
|
125
|
+
}
|
|
@@ -26,6 +26,13 @@ export declare function parsePyprojectDependencies(content: string): {
|
|
|
26
26
|
};
|
|
27
27
|
/** Requirement names (pins and ranges alike) from a requirements file, PEP 503 normalised. */
|
|
28
28
|
export declare function parseRequirementNames(content: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Requirement names from a requirements file and every file it includes with `-r` (confined to
|
|
31
|
+
* `root`, as the version reader confines them): an included file's pins are declared too. A pin
|
|
32
|
+
* in `-r sub/inner.txt` read as "Transitive dependency, not declared in your manifest" (final
|
|
33
|
+
* release audit of 6.2.5, 7g: jinja2==2.10).
|
|
34
|
+
*/
|
|
35
|
+
export declare function requirementNamesWithIncludes(file: string, root?: string, visited?: Set<string>): string[];
|
|
29
36
|
/** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
|
|
30
37
|
export declare function parseGoModDirectDeps(content: string): string[];
|
|
31
38
|
/**
|