@4da/mcp-server 6.0.2 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/README.md +26 -18
  3. package/dist/db.d.ts +73 -8
  4. package/dist/db.js +180 -43
  5. package/dist/doctor.d.ts +1 -1
  6. package/dist/doctor.js +15 -9
  7. package/dist/embeddings.js +19 -7
  8. package/dist/gitignore.js +9 -3
  9. package/dist/index.js +43 -4
  10. package/dist/live/cache.d.ts +31 -2
  11. package/dist/live/cache.js +113 -1
  12. package/dist/live/changelog-classify.d.ts +9 -3
  13. package/dist/live/changelog-classify.js +125 -12
  14. package/dist/live/changelog.d.ts +22 -3
  15. package/dist/live/changelog.js +132 -38
  16. package/dist/live/dev-scope.d.ts +2 -2
  17. package/dist/live/fix-paths.d.ts +118 -0
  18. package/dist/live/fix-paths.js +498 -0
  19. package/dist/live/github-releases.d.ts +69 -0
  20. package/dist/live/github-releases.js +252 -0
  21. package/dist/live/go-registry.d.ts +5 -0
  22. package/dist/live/go-registry.js +1 -1
  23. package/dist/live/go-releases.d.ts +48 -0
  24. package/dist/live/go-releases.js +196 -0
  25. package/dist/live/index.d.ts +34 -8
  26. package/dist/live/index.js +66 -7
  27. package/dist/live/lockfile-parents.d.ts +61 -0
  28. package/dist/live/lockfile-parents.js +360 -0
  29. package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
  30. package/dist/live/lockfile-parsers-pygo.js +178 -28
  31. package/dist/live/lockfile-parsers.d.ts +4 -1
  32. package/dist/live/lockfile-parsers.js +57 -24
  33. package/dist/live/lockfile-types.d.ts +12 -3
  34. package/dist/live/lockfile-types.js +2 -1
  35. package/dist/live/osv-ranges.d.ts +85 -0
  36. package/dist/live/osv-ranges.js +136 -0
  37. package/dist/live/osv-scanner.d.ts +1 -0
  38. package/dist/live/osv-scanner.js +85 -78
  39. package/dist/live/package-archive.d.ts +2 -0
  40. package/dist/live/package-archive.js +7 -4
  41. package/dist/live/pypi-releases.d.ts +44 -0
  42. package/dist/live/pypi-releases.js +103 -0
  43. package/dist/live/range-satisfies.d.ts +21 -0
  44. package/dist/live/range-satisfies.js +190 -0
  45. package/dist/live/rate-limiter.d.ts +6 -0
  46. package/dist/live/rate-limiter.js +5 -0
  47. package/dist/live/registry-health.js +2 -0
  48. package/dist/live/release-metadata.d.ts +17 -8
  49. package/dist/live/release-metadata.js +6 -3
  50. package/dist/live/resolution.d.ts +11 -1
  51. package/dist/live/resolution.js +16 -6
  52. package/dist/live/semver-utils.d.ts +8 -0
  53. package/dist/live/semver-utils.js +10 -2
  54. package/dist/live/types.d.ts +17 -1
  55. package/dist/live/upgrade-go.d.ts +62 -0
  56. package/dist/live/upgrade-go.js +130 -0
  57. package/dist/live/upgrade-pypi.d.ts +42 -0
  58. package/dist/live/upgrade-pypi.js +85 -0
  59. package/dist/live/upgrade-sources.d.ts +30 -11
  60. package/dist/live/upgrade-sources.js +93 -21
  61. package/dist/live/zip-archive.d.ts +22 -0
  62. package/dist/live/zip-archive.js +98 -0
  63. package/dist/memory-store.d.ts +19 -0
  64. package/dist/memory-store.js +8 -0
  65. package/dist/native-bindings.d.ts +7 -2
  66. package/dist/native-bindings.js +4 -0
  67. package/dist/project-tree.d.ts +50 -8
  68. package/dist/project-tree.js +121 -29
  69. package/dist/schema-registry.js +5 -5
  70. package/dist/server-instructions.d.ts +6 -0
  71. package/dist/server-instructions.js +20 -15
  72. package/dist/sqlite-driver.d.ts +134 -0
  73. package/dist/sqlite-driver.js +359 -0
  74. package/dist/tool-dispatch.d.ts +8 -1
  75. package/dist/tool-dispatch.js +17 -2
  76. package/dist/tools/agent-memory.js +10 -2
  77. package/dist/tools/app-plan.d.ts +11 -1
  78. package/dist/tools/app-plan.js +32 -6
  79. package/dist/tools/briefing-task-scope.d.ts +21 -2
  80. package/dist/tools/briefing-task-scope.js +97 -4
  81. package/dist/tools/decision-memory.js +9 -3
  82. package/dist/tools/dependency-check-signals.d.ts +23 -5
  83. package/dist/tools/dependency-check-signals.js +62 -19
  84. package/dist/tools/dependency-check-versions.d.ts +94 -0
  85. package/dist/tools/dependency-check-versions.js +213 -0
  86. package/dist/tools/dependency-check.d.ts +27 -11
  87. package/dist/tools/dependency-check.js +321 -74
  88. package/dist/tools/dependency-health.d.ts +1 -1
  89. package/dist/tools/dependency-health.js +5 -0
  90. package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
  91. package/dist/tools/knowledge-gap-ranges.js +1 -1
  92. package/dist/tools/knowledge-gap-scope.d.ts +2 -2
  93. package/dist/tools/project-scope.d.ts +45 -0
  94. package/dist/tools/project-scope.js +119 -0
  95. package/dist/tools/record-feedback.js +1 -1
  96. package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
  97. package/dist/tools/upgrade-impact-callsites.js +23 -5
  98. package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
  99. package/dist/tools/upgrade-impact-changelog.js +79 -0
  100. package/dist/tools/upgrade-impact-imports.d.ts +42 -0
  101. package/dist/tools/upgrade-impact-imports.js +260 -0
  102. package/dist/tools/upgrade-impact-literals.js +1 -1
  103. package/dist/tools/upgrade-impact-report.d.ts +23 -0
  104. package/dist/tools/upgrade-impact-report.js +45 -6
  105. package/dist/tools/upgrade-impact.d.ts +13 -4
  106. package/dist/tools/upgrade-impact.js +236 -85
  107. package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
  108. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  109. package/dist/tools/upgrade-planner.d.ts +31 -3
  110. package/dist/tools/upgrade-planner.js +74 -19
  111. package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
  112. package/dist/tools/vulnerability-scan-concise.js +15 -3
  113. package/dist/tools/vulnerability-scan-format.d.ts +58 -1
  114. package/dist/tools/vulnerability-scan-format.js +107 -11
  115. package/dist/tools/vulnerability-scan.d.ts +104 -13
  116. package/dist/tools/vulnerability-scan.js +27 -3
  117. package/dist/tools/what-should-i-know.d.ts +22 -2
  118. package/dist/tools/what-should-i-know.js +122 -12
  119. package/package.json +10 -32
@@ -18,9 +18,13 @@ import { PyPIRegistry } from "./pypi-registry.js";
18
18
  import { GoRegistry } from "./go-registry.js";
19
19
  import { NpmPackumentReader } from "./npm-packument.js";
20
20
  import { CratesVersionsReader } from "./crates-versions.js";
21
+ import { PypiReleasesReader } from "./pypi-releases.js";
22
+ import { GoReleasesReader } from "./go-releases.js";
23
+ import { FixPathSources } from "./fix-paths.js";
21
24
  import { fetchRegistryHealthFor } from "./registry-health.js";
22
25
  import { commonPathRoot, dedupeDependencies, emptyVulnResult } from "./dependency-set.js";
23
26
  import { groupIsStale, resolveGroup } from "./resolution.js";
27
+ import { mapEcosystem } from "./version-resolver.js";
24
28
  import { applyInstanceDevScope } from "./dev-scope.js";
25
29
  /** Minimum gap between vulnerability warmup attempts after an offline result. */
26
30
  const WARMUP_RETRY_MS = 60_000;
@@ -36,6 +40,9 @@ export class LiveIntelligence {
36
40
  goRegistry;
37
41
  npmPackuments;
38
42
  cratesVersions;
43
+ pypiReleases;
44
+ goReleases;
45
+ fixPathSources;
39
46
  enabled;
40
47
  lastVulnScan = null;
41
48
  /** Whether the stored scan included direct devDependencies. */
@@ -49,6 +56,8 @@ export class LiveIntelligence {
49
56
  * runs, 2026-10-03 pre-publish verification).
50
57
  */
51
58
  scanRequests = 0;
59
+ /** Scans running now, by generation, scope and path: a request for one of them awaits it. */
60
+ scansInFlight = new Map();
52
61
  storedScanRequest = 0;
53
62
  lastHeadlines = [];
54
63
  resolvedDeps = [];
@@ -57,6 +66,8 @@ export class LiveIntelligence {
57
66
  projectRoot = null;
58
67
  /** One entry per resolution group: its inputs, result, and the file signatures that say when it went stale. */
59
68
  groupResults = [];
69
+ /** The project-tree walk's report of what it left out, when the groups came from one. */
70
+ discovery = null;
60
71
  /** When the dependency set in use was assembled: at init, or at the last re-resolution. */
61
72
  resolvedAt = null;
62
73
  /**
@@ -86,6 +97,25 @@ export class LiveIntelligence {
86
97
  this.goRegistry = new GoRegistry(this.cache, this.rateLimiter);
87
98
  this.npmPackuments = new NpmPackumentReader(this.cache, this.rateLimiter);
88
99
  this.cratesVersions = new CratesVersionsReader(this.cache, this.rateLimiter);
100
+ this.pypiReleases = new PypiReleasesReader(this.cache, this.rateLimiter);
101
+ this.goReleases = new GoReleasesReader(this.cache, this.rateLimiter);
102
+ this.fixPathSources = new FixPathSources(this.cache, this.rateLimiter);
103
+ }
104
+ /** Advisory, registry and parent-requirement lookups for fix paths; null when offline. */
105
+ getFixPathSources() {
106
+ return this.enabled ? this.fixPathSources : null;
107
+ }
108
+ /**
109
+ * The lockfile one manifest directory resolved this ecosystem from, or null
110
+ * when it resolved from a manifest or nothing. `dir` is spelled as the
111
+ * resolution group spells it (the scan's sourceDirs).
112
+ */
113
+ lockfileFor(dir, ecosystem) {
114
+ for (const r of this.groupResults) {
115
+ if (r.group.dir === dir && mapEcosystem(r.group.language) === ecosystem && r.source?.kind === "lockfile")
116
+ return r.source.path;
117
+ }
118
+ return null;
89
119
  }
90
120
  /**
91
121
  * Initialize with project data. Call once after project scan.
@@ -114,8 +144,20 @@ export class LiveIntelligence {
114
144
  * project-tree.ts): each group resolves from its own directory and keeps
115
145
  * its manifest's platform targets; `root` is the reported project root.
116
146
  */
117
- initFromProjectTree(root, groups) {
147
+ initFromProjectTree(root, groups, discovery = null) {
118
148
  this.applyGroups(groups, root);
149
+ this.discovery = discovery;
150
+ }
151
+ /**
152
+ * Manifests with no lockfile beside them, whose ranged dependencies were not
153
+ * scanned: their versions are declared ranges, not installs.
154
+ */
155
+ getUnresolvedManifests() {
156
+ return this.groupResults.flatMap((r) => (r.unresolved ? [{ dir: r.group.dir, ...r.unresolved }] : []));
157
+ }
158
+ /** What the project-tree walk left out (lockfiles past its bounds); null when the set did not come from a walk. */
159
+ getDiscovery() {
160
+ return this.discovery;
119
161
  }
120
162
  /**
121
163
  * Initialize from dependency groups that each carry their own resolution
@@ -178,10 +220,12 @@ export class LiveIntelligence {
178
220
  const seen = new Set();
179
221
  const sources = [];
180
222
  for (const result of this.groupResults) {
181
- if (!result.source || seen.has(result.source.path))
182
- continue;
183
- seen.add(result.source.path);
184
- sources.push(result.source);
223
+ for (const source of result.sources) {
224
+ if (seen.has(source.path))
225
+ continue;
226
+ seen.add(source.path);
227
+ sources.push(source);
228
+ }
185
229
  }
186
230
  return { resolvedAt: this.resolvedAt, sources };
187
231
  }
@@ -222,13 +266,28 @@ export class LiveIntelligence {
222
266
  if (options?.forceRefresh) {
223
267
  this.cache.invalidateSource("osv");
224
268
  }
269
+ // The same scan already running (the startup warm-up, or a second call
270
+ // while the first is out) is awaited, not repeated. A forced refresh
271
+ // always runs its own.
272
+ const shareKey = `${this.generation}${options?.includeDev ? "dev" : "runtime"}${projectPath}`;
273
+ const running = options?.forceRefresh ? undefined : this.scansInFlight.get(shareKey);
274
+ if (running)
275
+ return running;
276
+ const scan = this.runScan(deps, projectPath, options?.includeDev ?? false);
277
+ if (!options?.forceRefresh) {
278
+ this.scansInFlight.set(shareKey, scan);
279
+ void scan.finally(() => this.scansInFlight.delete(shareKey));
280
+ }
281
+ return scan;
282
+ }
283
+ async runScan(deps, projectPath, includeDev) {
225
284
  const generation = this.generation;
226
285
  const request = ++this.scanRequests;
227
286
  try {
228
287
  const result = await this.osvScanner.scan(deps, projectPath);
229
288
  if (generation === this.generation && request > this.storedScanRequest) {
230
289
  this.lastVulnScan = result;
231
- this.lastVulnScanIncludesDev = options?.includeDev ?? false;
290
+ this.lastVulnScanIncludesDev = includeDev;
232
291
  this.storedScanRequest = request;
233
292
  }
234
293
  return result;
@@ -393,7 +452,7 @@ export class LiveIntelligence {
393
452
  }
394
453
  /** Per-release registry readers for dependency_check (name-only requests). */
395
454
  getReleaseSources() {
396
- return { npm: this.npmPackuments, "crates.io": this.cratesVersions };
455
+ return { npm: this.npmPackuments, "crates.io": this.cratesVersions, PyPI: this.pypiReleases, Go: this.goReleases };
397
456
  }
398
457
  /**
399
458
  * OSV advisories for explicit (ecosystem, name, version) triples that need
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Who pulls in one installed copy of a package, and with which declared
3
+ * requirement: the input to "is a lockfile refresh enough?".
4
+ *
5
+ * npm (package-lock), yarn and bun lockfiles record each parent's declared
6
+ * range. pnpm and Cargo lockfiles record only the resolved version, so their
7
+ * edges carry `range: null` and the caller looks the requirement up in the
8
+ * parent's registry metadata.
9
+ */
10
+ export type LockfileFormat = "package-lock" | "pnpm" | "yarn" | "yarn-berry" | "bun" | "cargo";
11
+ export interface ParentEdge {
12
+ /** The parent package, or the manifest's own name / directory for a project or workspace member. */
13
+ parent: string;
14
+ /** The parent's installed version; null for a project or workspace member. */
15
+ parentVersion: string | null;
16
+ /** The requirement as declared; null when the lockfile does not record it (pnpm, Cargo). */
17
+ range: string | null;
18
+ /** True when the parent is the project itself or a workspace member (then the package is direct there). */
19
+ local: boolean;
20
+ }
21
+ export interface LockfileParents {
22
+ format: LockfileFormat;
23
+ edges: ParentEdge[];
24
+ }
25
+ /** The lockfile format of a path, or null for a file this module does not read. */
26
+ export declare function lockfileFormat(file: string, content: string): LockfileFormat | null;
27
+ /** The parents of `name@version` in one lockfile, or null when the file is unreadable or not supported. */
28
+ export declare function readLockfileParents(file: string, name: string, version: string): LockfileParents | null;
29
+ interface LockEntry {
30
+ name?: string;
31
+ version?: string;
32
+ link?: boolean;
33
+ requires?: Record<string, string>;
34
+ dependencies?: Record<string, string> | Record<string, LockEntry>;
35
+ optionalDependencies?: Record<string, string>;
36
+ peerDependencies?: Record<string, string>;
37
+ }
38
+ export declare function packageLockParents(lock: {
39
+ packages?: Record<string, LockEntry>;
40
+ dependencies?: Record<string, LockEntry>;
41
+ name?: string;
42
+ }, name: string, version: string): ParentEdge[];
43
+ /**
44
+ * Parents from `packages:` (v5/v6) or `snapshots:` (v9) entries whose
45
+ * dependency blocks resolve `name` to `version`. pnpm records resolved
46
+ * versions only, so `range` is null. Importers (the project's own manifests)
47
+ * become local edges with their `specifier` when the lockfile records one.
48
+ */
49
+ export declare function pnpmParents(content: string, name: string, version: string): ParentEdge[];
50
+ export declare function yarnParents(content: string, name: string, version: string): ParentEdge[];
51
+ export declare function bunParents(lock: {
52
+ packages?: Record<string, unknown>;
53
+ workspaces?: Record<string, Record<string, Record<string, string>>>;
54
+ }, name: string, version: string): ParentEdge[];
55
+ /**
56
+ * Packages whose `dependencies` list this copy: "name" when only one version
57
+ * of it is in the lockfile, "name version" (optionally with a source) when
58
+ * several are. Cargo records no requirement, so `range` is null.
59
+ */
60
+ export declare function cargoParents(content: string, name: string, version: string): ParentEdge[];
61
+ export {};
@@ -0,0 +1,360 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Who pulls in one installed copy of a package, and with which declared
4
+ * requirement: the input to "is a lockfile refresh enough?".
5
+ *
6
+ * npm (package-lock), yarn and bun lockfiles record each parent's declared
7
+ * range. pnpm and Cargo lockfiles record only the resolved version, so their
8
+ * edges carry `range: null` and the caller looks the requirement up in the
9
+ * parent's registry metadata.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { stripTrailingCommas } from "./bun-lockfile.js";
14
+ import { parsePnpmKey } from "./js-lockfile-readers.js";
15
+ import { packageNameFromNodeModulesPath, parseCargoLockPackages } from "./lockfile-parsers.js";
16
+ const DEP_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
17
+ /** The lockfile format of a path, or null for a file this module does not read. */
18
+ export function lockfileFormat(file, content) {
19
+ const base = path.basename(file);
20
+ if (base === "package-lock.json" || base === "npm-shrinkwrap.json")
21
+ return "package-lock";
22
+ if (base === "pnpm-lock.yaml")
23
+ return "pnpm";
24
+ if (base === "yarn.lock")
25
+ return /^__metadata:/m.test(content) ? "yarn-berry" : "yarn";
26
+ if (base === "bun.lock")
27
+ return "bun";
28
+ if (base === "Cargo.lock")
29
+ return "cargo";
30
+ return null;
31
+ }
32
+ /** The parents of `name@version` in one lockfile, or null when the file is unreadable or not supported. */
33
+ export function readLockfileParents(file, name, version) {
34
+ let content;
35
+ try {
36
+ content = fs.readFileSync(file, "utf-8");
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ const format = lockfileFormat(file, content);
42
+ if (!format)
43
+ return null;
44
+ try {
45
+ switch (format) {
46
+ case "package-lock":
47
+ return { format, edges: packageLockParents(JSON.parse(content), name, version) };
48
+ case "pnpm":
49
+ return { format, edges: pnpmParents(content, name, version) };
50
+ case "yarn":
51
+ case "yarn-berry":
52
+ return { format, edges: yarnParents(content, name, version) };
53
+ case "bun":
54
+ return { format, edges: bunParents(JSON.parse(stripTrailingCommas(content)), name, version) };
55
+ case "cargo":
56
+ return { format, edges: cargoParents(content, name, version) };
57
+ }
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /** Node's lookup from install path `from`: its own node_modules, then each ancestor's, then the root's. */
64
+ function resolveInstallPath(packages, from, name) {
65
+ let dir = from;
66
+ for (;;) {
67
+ const candidate = `${dir ? `${dir}/` : ""}node_modules/${name}`;
68
+ if (candidate in packages)
69
+ return candidate;
70
+ if (dir === "")
71
+ return null;
72
+ const cut = dir.lastIndexOf("/node_modules/");
73
+ dir = cut >= 0 ? dir.slice(0, cut) : "";
74
+ }
75
+ }
76
+ export function packageLockParents(lock, name, version) {
77
+ const edges = [];
78
+ if (lock.packages && Object.keys(lock.packages).length > 0) {
79
+ const packages = lock.packages;
80
+ for (const [key, entry] of Object.entries(packages)) {
81
+ if (!entry || entry.link)
82
+ continue;
83
+ for (const field of DEP_FIELDS) {
84
+ const declared = entry[field];
85
+ const range = declared?.[name];
86
+ if (typeof range !== "string")
87
+ continue;
88
+ const at = resolveInstallPath(packages, key, name);
89
+ if (!at || packages[at]?.version !== version)
90
+ continue;
91
+ const local = !key.startsWith("node_modules/") && !key.includes("/node_modules/");
92
+ edges.push({
93
+ parent: local ? entry.name ?? (key || lock.name || ".") : packageNameFromNodeModulesPath(key),
94
+ parentVersion: local ? null : entry.version ?? null,
95
+ range,
96
+ local,
97
+ });
98
+ break;
99
+ }
100
+ }
101
+ return edges;
102
+ }
103
+ // v1: nested `dependencies` trees; each entry's `requires` names its ranges.
104
+ const walk = (deps, ancestors) => {
105
+ for (const [parentName, entry] of Object.entries(deps)) {
106
+ const nested = (entry.dependencies ?? {});
107
+ const range = entry.requires?.[name];
108
+ if (typeof range === "string") {
109
+ const scopes = [nested, ...ancestors];
110
+ const resolved = scopes.find((s) => s[name])?.[name];
111
+ if (resolved?.version === version) {
112
+ edges.push({ parent: parentName, parentVersion: entry.version ?? null, range, local: false });
113
+ }
114
+ }
115
+ if (Object.keys(nested).length > 0)
116
+ walk(nested, [nested, ...ancestors]);
117
+ }
118
+ };
119
+ if (lock.dependencies)
120
+ walk(lock.dependencies, [lock.dependencies]);
121
+ return edges;
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // pnpm-lock.yaml (v5, v6, v9)
125
+ // ---------------------------------------------------------------------------
126
+ function exactVersion(raw) {
127
+ const unquoted = raw.trim().replace(/^['"]|['"]$/g, "");
128
+ const v = unquoted.split("(")[0].split("_")[0].trim();
129
+ return /^\d/.test(v) ? v : null;
130
+ }
131
+ /**
132
+ * Parents from `packages:` (v5/v6) or `snapshots:` (v9) entries whose
133
+ * dependency blocks resolve `name` to `version`. pnpm records resolved
134
+ * versions only, so `range` is null. Importers (the project's own manifests)
135
+ * become local edges with their `specifier` when the lockfile records one.
136
+ */
137
+ export function pnpmParents(content, name, version) {
138
+ const edges = [];
139
+ const seen = new Set();
140
+ let section = "";
141
+ let parent = null;
142
+ let importer = null;
143
+ let inDeps = false;
144
+ let depIndent = -1;
145
+ let pendingImporterDep = false;
146
+ let importerSpecifier = null;
147
+ const add = (edge) => {
148
+ const key = `${edge.parent}\0${edge.parentVersion}`;
149
+ if (!seen.has(key)) {
150
+ seen.add(key);
151
+ edges.push(edge);
152
+ }
153
+ };
154
+ for (const line of content.split(/\r?\n/)) {
155
+ const text = line.trim();
156
+ if (text === "" || text.startsWith("#"))
157
+ continue;
158
+ const indent = line.length - line.trimStart().length;
159
+ if (indent === 0) {
160
+ section = text.endsWith(":") ? text.slice(0, -1) : "";
161
+ parent = null;
162
+ importer = section === "dependencies" || section === "devDependencies" || section === "optionalDependencies" ? "." : null;
163
+ inDeps = importer !== null;
164
+ depIndent = inDeps ? 2 : -1;
165
+ continue;
166
+ }
167
+ if (section === "packages" || section === "snapshots") {
168
+ if (indent === 2 && text.endsWith(":")) {
169
+ parent = parsePnpmKey(text.slice(0, -1));
170
+ inDeps = false;
171
+ }
172
+ else if (indent === 4 && parent) {
173
+ inDeps = /^(?:dependencies|optionalDependencies):$/.test(text);
174
+ }
175
+ else if (indent === 6 && parent && inDeps) {
176
+ const m = /^(?:'([^']+)'|"([^"]+)"|([^\s:'"]+)):\s*(.+)$/.exec(text);
177
+ if (m && (m[1] ?? m[2] ?? m[3]) === name && exactVersion(m[4]) === version) {
178
+ add({ parent: parent[0], parentVersion: parent[1], range: null, local: false });
179
+ }
180
+ }
181
+ continue;
182
+ }
183
+ if (section === "importers") {
184
+ if (indent === 2 && text.endsWith(":")) {
185
+ importer = text.slice(0, -1).replace(/^['"]|['"]$/g, "");
186
+ inDeps = false;
187
+ continue;
188
+ }
189
+ if (indent === 4) {
190
+ inDeps = /^(?:dependencies|devDependencies|optionalDependencies):$/.test(text);
191
+ depIndent = 4;
192
+ continue;
193
+ }
194
+ }
195
+ if (!inDeps || importer === null)
196
+ continue;
197
+ // A dependency of the project itself: v5 `name: 1.2.3`, v6/v9 `name:` + `specifier:` + `version:`.
198
+ if (indent === depIndent + 2) {
199
+ const m = /^(?:'([^']+)'|"([^"]+)"|([^\s:'"]+)):(?:\s+(.*))?$/.exec(text);
200
+ pendingImporterDep = !!m && (m[1] ?? m[2] ?? m[3]) === name;
201
+ importerSpecifier = null;
202
+ if (pendingImporterDep && m?.[4] && exactVersion(m[4]) === version) {
203
+ add({ parent: importer, parentVersion: null, range: null, local: true });
204
+ pendingImporterDep = false;
205
+ }
206
+ }
207
+ else if (indent === depIndent + 4 && pendingImporterDep) {
208
+ const spec = /^specifier:\s*(.+)$/.exec(text);
209
+ if (spec)
210
+ importerSpecifier = spec[1].replace(/^['"]|['"]$/g, "");
211
+ const ver = /^version:\s*(.+)$/.exec(text);
212
+ if (ver && exactVersion(ver[1]) === version) {
213
+ add({ parent: importer, parentVersion: null, range: importerSpecifier, local: true });
214
+ pendingImporterDep = false;
215
+ }
216
+ }
217
+ }
218
+ return edges;
219
+ }
220
+ // ---------------------------------------------------------------------------
221
+ // yarn.lock (v1 and berry)
222
+ // ---------------------------------------------------------------------------
223
+ const unquote = (s) => s.trim().replace(/^"|"$/g, "");
224
+ function parseYarnBlocks(content) {
225
+ const blocks = [];
226
+ for (const block of content.split(/\r?\n(?=\S)/)) {
227
+ const lines = block.split(/\r?\n/);
228
+ const header = lines[0];
229
+ const name = /^"?(@?[^@\s"]+)@/.exec(header)?.[1];
230
+ if (!name || name.startsWith("__"))
231
+ continue;
232
+ const version = /^\s+version:?\s+"?([^"\s]+)"?\s*$/m.exec(block)?.[1];
233
+ if (!version)
234
+ continue;
235
+ const specs = header.replace(/:\s*$/, "").split(",").map((s) => unquote(s).replace(/@npm:/, "@"));
236
+ const deps = [];
237
+ let inDeps = false;
238
+ let depIndent = -1;
239
+ for (const line of lines.slice(1)) {
240
+ if (!line.trim())
241
+ continue;
242
+ const indent = line.length - line.trimStart().length;
243
+ const text = line.trim();
244
+ if (/^(?:dependencies|optionalDependencies):?$/.test(text)) {
245
+ inDeps = true;
246
+ depIndent = indent;
247
+ continue;
248
+ }
249
+ if (inDeps && indent > depIndent) {
250
+ // v1: `name "range"`; berry: `name: "npm:range"` or `"@scope/x": range`.
251
+ const m = /^"?((?:@[^\s"/]+\/)?[^\s":]+)"?:?\s+"?([^"]+?)"?\s*$/.exec(text);
252
+ if (m)
253
+ deps.push([m[1], m[2]]);
254
+ }
255
+ else {
256
+ inDeps = false;
257
+ }
258
+ }
259
+ blocks.push({ name, version, specs, deps });
260
+ }
261
+ return blocks;
262
+ }
263
+ export function yarnParents(content, name, version) {
264
+ const blocks = parseYarnBlocks(content);
265
+ const resolved = new Map();
266
+ for (const b of blocks)
267
+ for (const spec of b.specs)
268
+ resolved.set(spec, b.version);
269
+ const edges = [];
270
+ for (const b of blocks) {
271
+ for (const [dep, rawRange] of b.deps) {
272
+ if (dep !== name)
273
+ continue;
274
+ const range = rawRange.replace(/^npm:/, "");
275
+ if (resolved.get(`${name}@${range}`) !== version)
276
+ continue;
277
+ edges.push({ parent: b.name, parentVersion: b.version, range, local: false });
278
+ }
279
+ }
280
+ return edges;
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // bun.lock
284
+ // ---------------------------------------------------------------------------
285
+ export function bunParents(lock, name, version) {
286
+ const packages = lock.packages ?? {};
287
+ const entryOf = (key) => {
288
+ const value = packages[key];
289
+ if (!Array.isArray(value) || typeof value[0] !== "string")
290
+ return null;
291
+ const at = value[0].lastIndexOf("@");
292
+ if (at <= 0)
293
+ return null;
294
+ const meta = value[2] && typeof value[2] === "object" ? value[2] : {};
295
+ return { name: value[0].slice(0, at), version: value[0].slice(at + 1), meta };
296
+ };
297
+ const segments = (key) => {
298
+ const parts = key.split("/");
299
+ const out = [];
300
+ for (let i = 0; i < parts.length; i++)
301
+ out.push(parts[i].startsWith("@") && i + 1 < parts.length ? `${parts[i]}/${parts[++i]}` : parts[i]);
302
+ return out;
303
+ };
304
+ const resolve = (fromKey) => {
305
+ const segs = segments(fromKey);
306
+ for (let n = segs.length; n > 0; n--) {
307
+ const candidate = [...segs.slice(0, n), name].join("/");
308
+ if (candidate in packages)
309
+ return candidate;
310
+ }
311
+ return name in packages ? name : null;
312
+ };
313
+ const edges = [];
314
+ for (const key of Object.keys(packages)) {
315
+ const e = entryOf(key);
316
+ if (!e)
317
+ continue;
318
+ const range = e.meta.dependencies?.[name] ?? e.meta.optionalDependencies?.[name] ?? e.meta.peerDependencies?.[name];
319
+ if (typeof range !== "string")
320
+ continue;
321
+ const at = resolve(key);
322
+ if (!at || entryOf(at)?.version !== version)
323
+ continue;
324
+ edges.push({ parent: e.name, parentVersion: e.version, range, local: false });
325
+ }
326
+ for (const [dir, ws] of Object.entries(lock.workspaces ?? {})) {
327
+ const range = ws.dependencies?.[name] ?? ws.devDependencies?.[name] ?? ws.optionalDependencies?.[name];
328
+ if (typeof range === "string" && entryOf(name)?.version === version) {
329
+ edges.push({ parent: dir || ".", parentVersion: null, range, local: true });
330
+ }
331
+ }
332
+ return edges;
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // Cargo.lock
336
+ // ---------------------------------------------------------------------------
337
+ /**
338
+ * Packages whose `dependencies` list this copy: "name" when only one version
339
+ * of it is in the lockfile, "name version" (optionally with a source) when
340
+ * several are. Cargo records no requirement, so `range` is null.
341
+ */
342
+ export function cargoParents(content, name, version) {
343
+ const packages = parseCargoLockPackages(content);
344
+ const versionsOf = packages.filter((p) => p.name === name).map((p) => p.version);
345
+ const cargoWritten = packages.some((p) => p.source !== null);
346
+ const edges = [];
347
+ for (const pkg of packages) {
348
+ for (const dep of pkg.dependencies) {
349
+ const [depName, depVersion] = dep.split(" ");
350
+ if (depName !== name)
351
+ continue;
352
+ const resolved = depVersion ?? (versionsOf.length === 1 ? versionsOf[0] : null);
353
+ if (resolved !== version)
354
+ continue;
355
+ const local = cargoWritten && pkg.source === null;
356
+ edges.push({ parent: pkg.name, parentVersion: local ? null : pkg.version, range: null, local });
357
+ }
358
+ }
359
+ return edges;
360
+ }
@@ -17,7 +17,38 @@
17
17
  import { InstanceSet, type VersionSource } from "./lockfile-types.js";
18
18
  /** PEP 503 normalised name: lowercase, runs of `-`, `_`, `.` become one `-`. */
19
19
  export declare function normalizePythonName(name: string): string;
20
+ /**
21
+ * Requirement files in a project directory: `*requirements*.txt` beside it
22
+ * (requirements-dev.txt, dev_requirements.txt, _test_minimum_requirements.txt)
23
+ * and every `.txt` in a `requirements/` folder (pip-compile-multi: superset
24
+ * keeps its 100 runtime pins in requirements/base.txt, which nothing read).
25
+ */
26
+ export declare function requirementFiles(cwd: string): string[];
27
+ /**
28
+ * Whether a requirements file is dev-only by name: requirements-dev.txt,
29
+ * dev-requirements.txt, requirements/testing.txt, _test_minimum_requirements.txt,
30
+ * docs-requirements.txt. The convention is near universal, but it is a name:
31
+ * a package any other file also lists keeps an unknown scope, never "dev".
32
+ */
33
+ export declare function devRequirementsFile(file: string): boolean;
34
+ /**
35
+ * Every Python source in a directory, merged: poetry.lock, uv.lock, pdm.lock,
36
+ * Pipfile.lock AND the requirement files. One source per ecosystem dropped
37
+ * whatever sat beside the first: langchain's libs/community has a
38
+ * poetry.lock and _test_minimum_requirements.txt, and only the lock was read.
39
+ * The version a direct dependency resolves to comes from a lockfile first.
40
+ */
20
41
  export declare function resolvePython(cwd: string): VersionSource;
42
+ /**
43
+ * Exact pins from one requirements file, following `-r`/`--requirement`
44
+ * includes (relative to the file) and applying `-c`/`--constraint` files:
45
+ * a constraints file installs nothing by itself, so its pins only give a
46
+ * version to a name the requirements list without one. Null when unreadable.
47
+ */
48
+ export declare function readRequirementsFile(file: string, visited?: Set<string>): Array<{
49
+ name: string;
50
+ version: string;
51
+ }> | null;
21
52
  /**
22
53
  * [[package]] tables from poetry.lock (any version) or uv.lock. A pre-1.2
23
54
  * poetry.lock records `category = "dev"` for dev-only packages; newer files