dep-oracle 1.2.0 → 1.3.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/README.md +33 -8
- package/dist/action/index.js +398 -16
- package/dist/badge-5Z3WAD2B.js +89 -0
- package/dist/badge-5Z3WAD2B.js.map +1 -0
- package/dist/chunk-32B3QIPY.js +1505 -0
- package/dist/chunk-32B3QIPY.js.map +1 -0
- package/dist/chunk-7DST6SNA.js +258 -0
- package/dist/chunk-7DST6SNA.js.map +1 -0
- package/dist/{chunk-TXSNFX3N.js → chunk-DLWG22RC.js} +403 -17
- package/dist/chunk-DLWG22RC.js.map +1 -0
- package/dist/chunk-HX6MGNBD.js +271 -0
- package/dist/chunk-HX6MGNBD.js.map +1 -0
- package/dist/chunk-IVXGOPRU.js +145 -0
- package/dist/chunk-IVXGOPRU.js.map +1 -0
- package/dist/chunk-SP3VYPXX.js +218 -0
- package/dist/chunk-SP3VYPXX.js.map +1 -0
- package/dist/chunk-T5EVLWZM.js +4234 -0
- package/dist/chunk-T5EVLWZM.js.map +1 -0
- package/dist/chunk-UMB5MJHL.js +239 -0
- package/dist/chunk-UMB5MJHL.js.map +1 -0
- package/dist/cli/index.js +163 -6499
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +9 -84
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +33 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/npm-UB54H37N.js +9 -0
- package/dist/npm-UB54H37N.js.map +1 -0
- package/dist/orchestrator-VOOYKDPT.js +8 -0
- package/dist/orchestrator-VOOYKDPT.js.map +1 -0
- package/dist/python-U4G2GK4J.js +9 -0
- package/dist/python-U4G2GK4J.js.map +1 -0
- package/dist/server-WONIBSG4.js +640 -0
- package/dist/server-WONIBSG4.js.map +1 -0
- package/dist/store-Z5UANEBB.js +8 -0
- package/dist/store-Z5UANEBB.js.map +1 -0
- package/dist/trust-score-YXYDFVPZ.js +8 -0
- package/dist/trust-score-YXYDFVPZ.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/chunk-TXSNFX3N.js.map +0 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BaseParser,
|
|
4
|
+
createDependencyNode,
|
|
5
|
+
createDependencyTree
|
|
6
|
+
} from "./chunk-UMB5MJHL.js";
|
|
7
|
+
|
|
8
|
+
// src/parsers/python.ts
|
|
9
|
+
import { readFile, access } from "fs/promises";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
async function fileExists(path) {
|
|
12
|
+
try {
|
|
13
|
+
await access(path);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function readText(path) {
|
|
20
|
+
return readFile(path, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
async function readJson(path) {
|
|
23
|
+
const raw = await readFile(path, "utf-8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
function normalizePyPIName(name) {
|
|
27
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
28
|
+
}
|
|
29
|
+
function parsePep508(spec) {
|
|
30
|
+
const cleaned = spec.replace(/\s+#.*$/, "").trim();
|
|
31
|
+
if (!cleaned || cleaned.startsWith("-")) return null;
|
|
32
|
+
const withoutMarkers = cleaned.split(";")[0].trim();
|
|
33
|
+
const match = withoutMarkers.match(
|
|
34
|
+
/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)(\[.*?\])?\s*(.*)?$/
|
|
35
|
+
);
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
const rawName = match[1];
|
|
38
|
+
const versionPart = (match[4] ?? "").trim();
|
|
39
|
+
const version = versionPart || "*";
|
|
40
|
+
return {
|
|
41
|
+
name: normalizePyPIName(rawName),
|
|
42
|
+
version
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function parseRequirementsTxt(content, tree) {
|
|
46
|
+
const lines = content.split("\n");
|
|
47
|
+
for (const rawLine of lines) {
|
|
48
|
+
const line = rawLine.trim();
|
|
49
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
50
|
+
const dep = parsePep508(line);
|
|
51
|
+
if (!dep) continue;
|
|
52
|
+
const key = `${dep.name}@${dep.version}`;
|
|
53
|
+
if (tree.nodes.has(key)) continue;
|
|
54
|
+
tree.nodes.set(
|
|
55
|
+
key,
|
|
56
|
+
createDependencyNode({
|
|
57
|
+
name: dep.name,
|
|
58
|
+
version: dep.version,
|
|
59
|
+
registry: "pypi",
|
|
60
|
+
depth: 0,
|
|
61
|
+
isDirect: true,
|
|
62
|
+
parent: null
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parsePyprojectDeps(content) {
|
|
68
|
+
const deps = [];
|
|
69
|
+
const lines = content.split("\n");
|
|
70
|
+
let inSection = false;
|
|
71
|
+
let inArray = false;
|
|
72
|
+
for (const rawLine of lines) {
|
|
73
|
+
const line = rawLine.trim();
|
|
74
|
+
if (/^\[project\]/.test(line)) {
|
|
75
|
+
inSection = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^\[tool\.poetry\.dependencies\]/.test(line)) {
|
|
79
|
+
inSection = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (inSection && /^\[/.test(line) && !/^\[project\]/.test(line) && !/^\[tool\.poetry\.dependencies\]/.test(line)) {
|
|
83
|
+
inSection = false;
|
|
84
|
+
inArray = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!inSection) continue;
|
|
88
|
+
if (/^dependencies\s*=\s*\[/.test(line)) {
|
|
89
|
+
inArray = true;
|
|
90
|
+
const inlineMatch = line.match(/\[(.+)\]/);
|
|
91
|
+
if (inlineMatch) {
|
|
92
|
+
const entries = inlineMatch[1].split(",").map((s) => s.trim().replace(/^"|"$/g, "").replace(/^'|'$/g, ""));
|
|
93
|
+
deps.push(...entries.filter(Boolean));
|
|
94
|
+
inArray = false;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (inArray) {
|
|
99
|
+
if (line === "]") {
|
|
100
|
+
inArray = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const cleaned = line.replace(/,$/g, "").replace(/^"|"$/g, "").replace(/^'|'$/g, "").trim();
|
|
104
|
+
if (cleaned) {
|
|
105
|
+
deps.push(cleaned);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (inSection && !inArray && line.includes("=") && !line.startsWith("[")) {
|
|
110
|
+
const eqIdx = line.indexOf("=");
|
|
111
|
+
const key = line.slice(0, eqIdx).trim();
|
|
112
|
+
const valRaw = line.slice(eqIdx + 1).trim();
|
|
113
|
+
if (key === "python") continue;
|
|
114
|
+
if (!valRaw) continue;
|
|
115
|
+
let version = "*";
|
|
116
|
+
const strMatch = valRaw.match(/^"([^"]+)"|^'([^']+)'/);
|
|
117
|
+
if (strMatch) {
|
|
118
|
+
version = strMatch[1] ?? strMatch[2] ?? "*";
|
|
119
|
+
}
|
|
120
|
+
const tableMatch = valRaw.match(/version\s*=\s*"([^"]+)"/);
|
|
121
|
+
if (tableMatch) {
|
|
122
|
+
version = tableMatch[1];
|
|
123
|
+
}
|
|
124
|
+
deps.push(`${key}${version !== "*" ? version : ""}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return deps;
|
|
128
|
+
}
|
|
129
|
+
function addPyprojectDeps(content, tree) {
|
|
130
|
+
const specs = parsePyprojectDeps(content);
|
|
131
|
+
for (const spec of specs) {
|
|
132
|
+
const dep = parsePep508(spec);
|
|
133
|
+
if (!dep) continue;
|
|
134
|
+
const key = `${dep.name}@${dep.version}`;
|
|
135
|
+
if (tree.nodes.has(key)) continue;
|
|
136
|
+
tree.nodes.set(
|
|
137
|
+
key,
|
|
138
|
+
createDependencyNode({
|
|
139
|
+
name: dep.name,
|
|
140
|
+
version: dep.version,
|
|
141
|
+
registry: "pypi",
|
|
142
|
+
depth: 0,
|
|
143
|
+
isDirect: true,
|
|
144
|
+
parent: null
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function parsePipfileLock(lockData, tree) {
|
|
150
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
151
|
+
for (const node of tree.nodes.values()) {
|
|
152
|
+
if (node.isDirect) {
|
|
153
|
+
directNames.add(node.name);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const sections = [
|
|
157
|
+
lockData.default,
|
|
158
|
+
lockData.develop
|
|
159
|
+
];
|
|
160
|
+
for (const section of sections) {
|
|
161
|
+
if (!section) continue;
|
|
162
|
+
for (const [rawName, meta] of Object.entries(section)) {
|
|
163
|
+
const name = normalizePyPIName(rawName);
|
|
164
|
+
const version = (meta.version ?? "*").replace(/^==/, "");
|
|
165
|
+
const isDirect = directNames.has(name);
|
|
166
|
+
const key = `${name}@${version}`;
|
|
167
|
+
if (tree.nodes.has(key)) continue;
|
|
168
|
+
tree.nodes.set(
|
|
169
|
+
key,
|
|
170
|
+
createDependencyNode({
|
|
171
|
+
name,
|
|
172
|
+
version,
|
|
173
|
+
registry: "pypi",
|
|
174
|
+
depth: isDirect ? 0 : 1,
|
|
175
|
+
isDirect,
|
|
176
|
+
parent: null
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function parsePoetryLock(content, tree) {
|
|
183
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const node of tree.nodes.values()) {
|
|
185
|
+
if (node.isDirect) {
|
|
186
|
+
directNames.add(node.name);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const blocks = content.split(/^\[\[package\]\]\s*$/m);
|
|
190
|
+
for (const block of blocks) {
|
|
191
|
+
const nameMatch = block.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
192
|
+
const versionMatch = block.match(/^\s*version\s*=\s*"([^"]+)"/m);
|
|
193
|
+
if (!nameMatch) continue;
|
|
194
|
+
const name = normalizePyPIName(nameMatch[1]);
|
|
195
|
+
const version = versionMatch ? versionMatch[1] : "*";
|
|
196
|
+
const isDirect = directNames.has(name);
|
|
197
|
+
const key = `${name}@${version}`;
|
|
198
|
+
if (tree.nodes.has(key)) continue;
|
|
199
|
+
tree.nodes.set(
|
|
200
|
+
key,
|
|
201
|
+
createDependencyNode({
|
|
202
|
+
name,
|
|
203
|
+
version,
|
|
204
|
+
registry: "pypi",
|
|
205
|
+
depth: isDirect ? 0 : 1,
|
|
206
|
+
isDirect,
|
|
207
|
+
parent: null
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
var PythonParser = class extends BaseParser {
|
|
213
|
+
name = "python";
|
|
214
|
+
async detect(dir) {
|
|
215
|
+
const checks = await Promise.all([
|
|
216
|
+
fileExists(join(dir, "requirements.txt")),
|
|
217
|
+
fileExists(join(dir, "pyproject.toml")),
|
|
218
|
+
fileExists(join(dir, "Pipfile")),
|
|
219
|
+
fileExists(join(dir, "poetry.lock"))
|
|
220
|
+
]);
|
|
221
|
+
return checks.some(Boolean);
|
|
222
|
+
}
|
|
223
|
+
async parse(dir) {
|
|
224
|
+
const reqPath = join(dir, "requirements.txt");
|
|
225
|
+
const pyprojectPath = join(dir, "pyproject.toml");
|
|
226
|
+
const pipfileLockPath = join(dir, "Pipfile.lock");
|
|
227
|
+
const poetryLockPath = join(dir, "poetry.lock");
|
|
228
|
+
let manifestPath = dir;
|
|
229
|
+
if (await fileExists(pyprojectPath)) {
|
|
230
|
+
manifestPath = pyprojectPath;
|
|
231
|
+
} else if (await fileExists(reqPath)) {
|
|
232
|
+
manifestPath = reqPath;
|
|
233
|
+
}
|
|
234
|
+
const tree = createDependencyTree("python-project", manifestPath);
|
|
235
|
+
if (await fileExists(reqPath)) {
|
|
236
|
+
const content = await readText(reqPath);
|
|
237
|
+
parseRequirementsTxt(content, tree);
|
|
238
|
+
}
|
|
239
|
+
if (await fileExists(pyprojectPath)) {
|
|
240
|
+
const content = await readText(pyprojectPath);
|
|
241
|
+
addPyprojectDeps(content, tree);
|
|
242
|
+
const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
243
|
+
if (nameMatch) {
|
|
244
|
+
tree.root = normalizePyPIName(nameMatch[1]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (await fileExists(pipfileLockPath)) {
|
|
248
|
+
const lockData = await readJson(pipfileLockPath);
|
|
249
|
+
parsePipfileLock(lockData, tree);
|
|
250
|
+
}
|
|
251
|
+
if (await fileExists(poetryLockPath)) {
|
|
252
|
+
const content = await readText(poetryLockPath);
|
|
253
|
+
parsePoetryLock(content, tree);
|
|
254
|
+
}
|
|
255
|
+
tree.totalDirect = 0;
|
|
256
|
+
tree.totalTransitive = 0;
|
|
257
|
+
for (const node of tree.nodes.values()) {
|
|
258
|
+
if (node.isDirect) {
|
|
259
|
+
tree.totalDirect++;
|
|
260
|
+
} else {
|
|
261
|
+
tree.totalTransitive++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return tree;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export {
|
|
269
|
+
PythonParser
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=chunk-HX6MGNBD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/parsers/python.ts"],"sourcesContent":["import { readFile, access } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { BaseParser } from \"./base.js\";\nimport {\n type DependencyTree,\n createDependencyTree,\n createDependencyNode,\n} from \"./schema.js\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function readText(path: string): Promise<string> {\n return readFile(path, \"utf-8\");\n}\n\nasync function readJson<T = unknown>(path: string): Promise<T> {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n}\n\n/**\n * Normalize a Python package name according to PEP 503.\n * Replaces runs of [-_.] with a single hyphen and lowercases.\n */\nfunction normalizePyPIName(name: string): string {\n return name.toLowerCase().replace(/[-_.]+/g, \"-\");\n}\n\n/**\n * Parse a single PEP 508 dependency specifier and return (name, version).\n *\n * Handles:\n * requests==2.31.0\n * flask>=2.0,<3.0\n * django~=4.2\n * numpy\n * black[jupyter]>=23.0\n * urllib3 ; python_version >= \"3.7\"\n */\nfunction parsePep508(spec: string): { name: string; version: string } | null {\n // Strip inline comments\n const cleaned = spec.replace(/\\s+#.*$/, '').trim();\n if (!cleaned || cleaned.startsWith(\"-\")) return null;\n\n // Strip environment markers (everything after a bare \";\")\n const withoutMarkers = cleaned.split(\";\")[0].trim();\n\n // Match: name[extras](==|>=|<=|~=|!=|>|<)version(,...)\n const match = withoutMarkers.match(\n /^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)(\\[.*?\\])?\\s*(.*)?$/,\n );\n if (!match) return null;\n\n const rawName = match[1];\n const versionPart = (match[4] ?? \"\").trim();\n\n // Clean version: take the full constraint string as the version\n const version = versionPart || \"*\";\n\n return {\n name: normalizePyPIName(rawName),\n version,\n };\n}\n\n// ---------------------------------------------------------------------------\n// requirements.txt parser\n// ---------------------------------------------------------------------------\n\nfunction parseRequirementsTxt(\n content: string,\n tree: DependencyTree,\n): void {\n const lines = content.split(\"\\n\");\n for (const rawLine of lines) {\n const line = rawLine.trim();\n // Skip comments, blank lines, and option lines\n if (!line || line.startsWith(\"#\") || line.startsWith(\"-\")) continue;\n\n const dep = parsePep508(line);\n if (!dep) continue;\n\n const key = `${dep.name}@${dep.version}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name: dep.name,\n version: dep.version,\n registry: \"pypi\",\n depth: 0,\n isDirect: true,\n parent: null,\n }),\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// pyproject.toml parser (lightweight, regex-based to avoid TOML dep)\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the [project.dependencies] array from pyproject.toml content.\n *\n * We use a simple state-machine approach instead of a full TOML parser to\n * keep the dependency count at zero for parsing logic.\n */\nfunction parsePyprojectDeps(content: string): string[] {\n const deps: string[] = [];\n const lines = content.split(\"\\n\");\n let inSection = false;\n let inArray = false;\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n\n // Detect [project] section's dependencies key\n if (/^\\[project\\]/.test(line)) {\n inSection = true;\n continue;\n }\n\n // Detect [tool.poetry.dependencies] section\n if (/^\\[tool\\.poetry\\.dependencies\\]/.test(line)) {\n inSection = true;\n continue;\n }\n\n // If we hit a new section header, stop\n if (inSection && /^\\[/.test(line) && !/^\\[project\\]/.test(line) && !/^\\[tool\\.poetry\\.dependencies\\]/.test(line)) {\n inSection = false;\n inArray = false;\n continue;\n }\n\n if (!inSection) continue;\n\n // Inside [project] look for `dependencies = [`\n if (/^dependencies\\s*=\\s*\\[/.test(line)) {\n inArray = true;\n // Check if entries are on the same line: dependencies = [\"foo\", \"bar\"]\n const inlineMatch = line.match(/\\[(.+)\\]/);\n if (inlineMatch) {\n const entries = inlineMatch[1]\n .split(\",\")\n .map((s) => s.trim().replace(/^\"|\"$/g, \"\").replace(/^'|'$/g, \"\"));\n deps.push(...entries.filter(Boolean));\n inArray = false;\n }\n continue;\n }\n\n // Inside the dependencies array, collect entries\n if (inArray) {\n if (line === \"]\") {\n inArray = false;\n continue;\n }\n // Each line is like \"requests>=2.28\" or 'flask~=2.0',\n const cleaned = line.replace(/,$/g, \"\").replace(/^\"|\"$/g, \"\").replace(/^'|'$/g, \"\").trim();\n if (cleaned) {\n deps.push(cleaned);\n }\n continue;\n }\n\n // Inside [tool.poetry.dependencies] entries look like:\n // requests = \"^2.28\"\n // python = \"^3.10\"\n // flask = {version = \"~2.0\", optional = true}\n if (inSection && !inArray && line.includes(\"=\") && !line.startsWith(\"[\")) {\n const eqIdx = line.indexOf(\"=\");\n const key = line.slice(0, eqIdx).trim();\n const valRaw = line.slice(eqIdx + 1).trim();\n\n // Skip the python version constraint\n if (key === \"python\") continue;\n // Skip empty or non-string values\n if (!valRaw) continue;\n\n let version = \"*\";\n // Simple string value: \"^2.28\"\n const strMatch = valRaw.match(/^\"([^\"]+)\"|^'([^']+)'/);\n if (strMatch) {\n version = strMatch[1] ?? strMatch[2] ?? \"*\";\n }\n // Inline table: {version = \"~2.0\", ...}\n const tableMatch = valRaw.match(/version\\s*=\\s*\"([^\"]+)\"/);\n if (tableMatch) {\n version = tableMatch[1];\n }\n\n deps.push(`${key}${version !== \"*\" ? version : \"\"}`);\n }\n }\n\n return deps;\n}\n\nfunction addPyprojectDeps(content: string, tree: DependencyTree): void {\n const specs = parsePyprojectDeps(content);\n for (const spec of specs) {\n const dep = parsePep508(spec);\n if (!dep) continue;\n\n const key = `${dep.name}@${dep.version}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name: dep.name,\n version: dep.version,\n registry: \"pypi\",\n depth: 0,\n isDirect: true,\n parent: null,\n }),\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Pipfile.lock parser (transitive dependencies)\n// ---------------------------------------------------------------------------\n\ninterface PipfileLockJson {\n default?: Record<string, { version?: string }>;\n develop?: Record<string, { version?: string }>;\n}\n\nfunction parsePipfileLock(\n lockData: PipfileLockJson,\n tree: DependencyTree,\n): void {\n const directNames = new Set<string>();\n for (const node of tree.nodes.values()) {\n if (node.isDirect) {\n directNames.add(node.name);\n }\n }\n\n const sections: Array<Record<string, { version?: string }> | undefined> = [\n lockData.default,\n lockData.develop,\n ];\n\n for (const section of sections) {\n if (!section) continue;\n for (const [rawName, meta] of Object.entries(section)) {\n const name = normalizePyPIName(rawName);\n // Version in Pipfile.lock is like \"==2.31.0\"\n const version = (meta.version ?? \"*\").replace(/^==/, \"\");\n const isDirect = directNames.has(name);\n\n const key = `${name}@${version}`;\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name,\n version,\n registry: \"pypi\",\n depth: isDirect ? 0 : 1,\n isDirect,\n parent: null,\n }),\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// poetry.lock parser (regex-based, no TOML library)\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a poetry.lock file to extract package names and versions.\n *\n * poetry.lock uses a TOML-like format with repeated `[[package]]` blocks.\n * Each block has `name = \"...\"` and `version = \"...\"` lines. We use regex\n * to split on `[[package]]` headers and extract the fields we need.\n *\n * Packages that match an already-known direct dependency (from pyproject.toml\n * or requirements.txt) are marked `isDirect: true, depth: 0`. All others are\n * treated as transitive (`depth: 1`).\n */\nfunction parsePoetryLock(content: string, tree: DependencyTree): void {\n // Collect names of already-known direct dependencies so we can cross-ref\n const directNames = new Set<string>();\n for (const node of tree.nodes.values()) {\n if (node.isDirect) {\n directNames.add(node.name);\n }\n }\n\n // Split on [[package]] headers. The first element is everything before the\n // first header (metadata / preamble) which we skip.\n const blocks = content.split(/^\\[\\[package\\]\\]\\s*$/m);\n\n for (const block of blocks) {\n const nameMatch = block.match(/^\\s*name\\s*=\\s*\"([^\"]+)\"/m);\n const versionMatch = block.match(/^\\s*version\\s*=\\s*\"([^\"]+)\"/m);\n\n if (!nameMatch) continue;\n\n const name = normalizePyPIName(nameMatch[1]);\n const version = versionMatch ? versionMatch[1] : \"*\";\n\n const isDirect = directNames.has(name);\n const key = `${name}@${version}`;\n\n if (tree.nodes.has(key)) continue;\n\n tree.nodes.set(\n key,\n createDependencyNode({\n name,\n version,\n registry: \"pypi\",\n depth: isDirect ? 0 : 1,\n isDirect,\n parent: null,\n }),\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// PythonParser\n// ---------------------------------------------------------------------------\n\nexport class PythonParser extends BaseParser {\n readonly name = \"python\";\n\n async detect(dir: string): Promise<boolean> {\n const checks = await Promise.all([\n fileExists(join(dir, \"requirements.txt\")),\n fileExists(join(dir, \"pyproject.toml\")),\n fileExists(join(dir, \"Pipfile\")),\n fileExists(join(dir, \"poetry.lock\")),\n ]);\n return checks.some(Boolean);\n }\n\n async parse(dir: string): Promise<DependencyTree> {\n const reqPath = join(dir, \"requirements.txt\");\n const pyprojectPath = join(dir, \"pyproject.toml\");\n const pipfileLockPath = join(dir, \"Pipfile.lock\");\n const poetryLockPath = join(dir, \"poetry.lock\");\n\n // Determine manifest path for the tree metadata\n let manifestPath = dir;\n if (await fileExists(pyprojectPath)) {\n manifestPath = pyprojectPath;\n } else if (await fileExists(reqPath)) {\n manifestPath = reqPath;\n }\n\n const tree = createDependencyTree(\"python-project\", manifestPath);\n\n // Parse requirements.txt if it exists\n if (await fileExists(reqPath)) {\n const content = await readText(reqPath);\n parseRequirementsTxt(content, tree);\n }\n\n // Parse pyproject.toml if it exists (may add more deps)\n if (await fileExists(pyprojectPath)) {\n const content = await readText(pyprojectPath);\n addPyprojectDeps(content, tree);\n\n // Try to extract project name from pyproject.toml\n const nameMatch = content.match(/^\\s*name\\s*=\\s*\"([^\"]+)\"/m);\n if (nameMatch) {\n tree.root = normalizePyPIName(nameMatch[1]);\n }\n }\n\n // If Pipfile.lock exists, parse transitive dependencies from it\n if (await fileExists(pipfileLockPath)) {\n const lockData = await readJson<PipfileLockJson>(pipfileLockPath);\n parsePipfileLock(lockData, tree);\n }\n\n // If poetry.lock exists, parse locked dependencies from it\n if (await fileExists(poetryLockPath)) {\n const content = await readText(poetryLockPath);\n parsePoetryLock(content, tree);\n }\n\n // Recount totals\n tree.totalDirect = 0;\n tree.totalTransitive = 0;\n for (const node of tree.nodes.values()) {\n if (node.isDirect) {\n tree.totalDirect++;\n } else {\n tree.totalTransitive++;\n }\n }\n\n return tree;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cache/store.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
var DEFAULT_TTL_SECONDS = 86400;
|
|
8
|
+
var CACHE_DIR = join(homedir(), ".dep-oracle");
|
|
9
|
+
var CACHE_PATH = join(CACHE_DIR, "cache.json");
|
|
10
|
+
var CacheManager = class {
|
|
11
|
+
store;
|
|
12
|
+
filePath;
|
|
13
|
+
constructor(filePath = CACHE_PATH) {
|
|
14
|
+
this.filePath = filePath;
|
|
15
|
+
const dir = filePath === CACHE_PATH ? CACHE_DIR : join(filePath, "..");
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
this.store = this.load();
|
|
18
|
+
}
|
|
19
|
+
// -----------------------------------------------------------------------
|
|
20
|
+
// Public API
|
|
21
|
+
// -----------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Retrieve a cached value by key. Returns `null` when the key does not
|
|
24
|
+
* exist or has expired.
|
|
25
|
+
*/
|
|
26
|
+
get(key) {
|
|
27
|
+
const entry = this.store[key];
|
|
28
|
+
if (!entry) return null;
|
|
29
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
30
|
+
if (now - entry.createdAt > entry.ttl) {
|
|
31
|
+
delete this.store[key];
|
|
32
|
+
this.persist();
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return entry.value;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Store a value in the cache.
|
|
39
|
+
*/
|
|
40
|
+
set(key, value, ttl = DEFAULT_TTL_SECONDS) {
|
|
41
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
42
|
+
this.store[key] = { value, createdAt: now, ttl };
|
|
43
|
+
this.persist();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check whether a non-expired entry exists for the given key.
|
|
47
|
+
*/
|
|
48
|
+
has(key) {
|
|
49
|
+
return this.get(key) !== null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Delete all entries from the cache.
|
|
53
|
+
*/
|
|
54
|
+
clear() {
|
|
55
|
+
this.store = {};
|
|
56
|
+
this.persist();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Remove all expired entries.
|
|
60
|
+
*/
|
|
61
|
+
cleanup() {
|
|
62
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
63
|
+
let removed = 0;
|
|
64
|
+
for (const [key, entry] of Object.entries(this.store)) {
|
|
65
|
+
if (now - entry.createdAt > entry.ttl) {
|
|
66
|
+
delete this.store[key];
|
|
67
|
+
removed++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (removed > 0) this.persist();
|
|
71
|
+
return removed;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Return a human-readable string describing how old the cached entry is.
|
|
75
|
+
*/
|
|
76
|
+
getCacheAge(key) {
|
|
77
|
+
const entry = this.store[key];
|
|
78
|
+
if (!entry) return null;
|
|
79
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
80
|
+
const ageSeconds = now - entry.createdAt;
|
|
81
|
+
if (ageSeconds < 60) return "just now";
|
|
82
|
+
const ageMinutes = Math.floor(ageSeconds / 60);
|
|
83
|
+
if (ageMinutes < 60) {
|
|
84
|
+
return `${ageMinutes} minute${ageMinutes === 1 ? "" : "s"} ago`;
|
|
85
|
+
}
|
|
86
|
+
const ageHours = Math.floor(ageMinutes / 60);
|
|
87
|
+
if (ageHours < 24) {
|
|
88
|
+
return `${ageHours} hour${ageHours === 1 ? "" : "s"} ago`;
|
|
89
|
+
}
|
|
90
|
+
const ageDays = Math.floor(ageHours / 24);
|
|
91
|
+
return `${ageDays} day${ageDays === 1 ? "" : "s"} ago`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Return the ISO-8601 timestamp of when a key was cached, or `null`.
|
|
95
|
+
*/
|
|
96
|
+
getCachedAt(key) {
|
|
97
|
+
const entry = this.store[key];
|
|
98
|
+
if (!entry) return null;
|
|
99
|
+
return new Date(entry.createdAt * 1e3).toISOString();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Return total number of (non-expired) entries in the cache.
|
|
103
|
+
*/
|
|
104
|
+
size() {
|
|
105
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
106
|
+
let count = 0;
|
|
107
|
+
for (const entry of Object.values(this.store)) {
|
|
108
|
+
if (now - entry.createdAt <= entry.ttl) count++;
|
|
109
|
+
}
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* No-op for API compatibility. JSON cache does not need explicit closing.
|
|
114
|
+
*/
|
|
115
|
+
close() {
|
|
116
|
+
}
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
// Persistence
|
|
119
|
+
// -----------------------------------------------------------------------
|
|
120
|
+
load() {
|
|
121
|
+
try {
|
|
122
|
+
if (existsSync(this.filePath)) {
|
|
123
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
124
|
+
return JSON.parse(raw);
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof SyntaxError) {
|
|
128
|
+
} else {
|
|
129
|
+
console.error(`Cache load error: ${err instanceof Error ? err.message : String(err)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
persist() {
|
|
135
|
+
try {
|
|
136
|
+
writeFileSync(this.filePath, JSON.stringify(this.store), "utf-8");
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
CacheManager
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=chunk-IVXGOPRU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache/store.ts"],"sourcesContent":["/**\n * JSON file-based cache with TTL support.\n *\n * Stores data at `~/.dep-oracle/cache.json`. Zero native dependencies —\n * works with `npx` on every OS without build tools.\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_TTL_SECONDS = 86_400; // 24 hours\nconst CACHE_DIR = join(homedir(), \".dep-oracle\");\nconst CACHE_PATH = join(CACHE_DIR, \"cache.json\");\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ninterface CacheEntry {\n value: unknown;\n createdAt: number; // epoch seconds\n ttl: number; // seconds\n}\n\ntype CacheStore = Record<string, CacheEntry>;\n\n// ---------------------------------------------------------------------------\n// CacheManager\n// ---------------------------------------------------------------------------\n\nexport class CacheManager {\n private store: CacheStore;\n private readonly filePath: string;\n\n constructor(filePath: string = CACHE_PATH) {\n this.filePath = filePath;\n\n // Ensure directory exists\n const dir = filePath === CACHE_PATH ? CACHE_DIR : join(filePath, \"..\");\n mkdirSync(dir, { recursive: true });\n\n // Load existing cache\n this.store = this.load();\n }\n\n // -----------------------------------------------------------------------\n // Public API\n // -----------------------------------------------------------------------\n\n /**\n * Retrieve a cached value by key. Returns `null` when the key does not\n * exist or has expired.\n */\n get<T>(key: string): T | null {\n const entry = this.store[key];\n if (!entry) return null;\n\n const now = Math.floor(Date.now() / 1000);\n if (now - entry.createdAt > entry.ttl) {\n delete this.store[key];\n this.persist();\n return null;\n }\n\n return entry.value as T;\n }\n\n /**\n * Store a value in the cache.\n */\n set<T>(key: string, value: T, ttl: number = DEFAULT_TTL_SECONDS): void {\n const now = Math.floor(Date.now() / 1000);\n this.store[key] = { value, createdAt: now, ttl };\n this.persist();\n }\n\n /**\n * Check whether a non-expired entry exists for the given key.\n */\n has(key: string): boolean {\n return this.get<unknown>(key) !== null;\n }\n\n /**\n * Delete all entries from the cache.\n */\n clear(): void {\n this.store = {};\n this.persist();\n }\n\n /**\n * Remove all expired entries.\n */\n cleanup(): number {\n const now = Math.floor(Date.now() / 1000);\n let removed = 0;\n\n for (const [key, entry] of Object.entries(this.store)) {\n if (now - entry.createdAt > entry.ttl) {\n delete this.store[key];\n removed++;\n }\n }\n\n if (removed > 0) this.persist();\n return removed;\n }\n\n /**\n * Return a human-readable string describing how old the cached entry is.\n */\n getCacheAge(key: string): string | null {\n const entry = this.store[key];\n if (!entry) return null;\n\n const now = Math.floor(Date.now() / 1000);\n const ageSeconds = now - entry.createdAt;\n\n if (ageSeconds < 60) return \"just now\";\n\n const ageMinutes = Math.floor(ageSeconds / 60);\n if (ageMinutes < 60) {\n return `${ageMinutes} minute${ageMinutes === 1 ? \"\" : \"s\"} ago`;\n }\n\n const ageHours = Math.floor(ageMinutes / 60);\n if (ageHours < 24) {\n return `${ageHours} hour${ageHours === 1 ? \"\" : \"s\"} ago`;\n }\n\n const ageDays = Math.floor(ageHours / 24);\n return `${ageDays} day${ageDays === 1 ? \"\" : \"s\"} ago`;\n }\n\n /**\n * Return the ISO-8601 timestamp of when a key was cached, or `null`.\n */\n getCachedAt(key: string): string | null {\n const entry = this.store[key];\n if (!entry) return null;\n return new Date(entry.createdAt * 1000).toISOString();\n }\n\n /**\n * Return total number of (non-expired) entries in the cache.\n */\n size(): number {\n const now = Math.floor(Date.now() / 1000);\n let count = 0;\n for (const entry of Object.values(this.store)) {\n if (now - entry.createdAt <= entry.ttl) count++;\n }\n return count;\n }\n\n /**\n * No-op for API compatibility. JSON cache does not need explicit closing.\n */\n close(): void {\n // Nothing to close — included for drop-in compatibility\n }\n\n // -----------------------------------------------------------------------\n // Persistence\n // -----------------------------------------------------------------------\n\n private load(): CacheStore {\n try {\n if (existsSync(this.filePath)) {\n const raw = readFileSync(this.filePath, \"utf-8\");\n return JSON.parse(raw) as CacheStore;\n }\n } catch (err) {\n if (err instanceof SyntaxError) {\n // Corrupted JSON — start fresh\n } else {\n // Log unexpected errors but continue with empty cache\n console.error(`Cache load error: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n return {};\n }\n\n private persist(): void {\n try {\n writeFileSync(this.filePath, JSON.stringify(this.store), \"utf-8\");\n } catch {\n // Best-effort — cache persistence should never crash the process\n }\n }\n}\n"],"mappings":"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","names":[]}
|