@agentxm/extension-discovery 0.28.4-bootstrap.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/LICENSE +110 -0
- package/dist/src/discover.d.ts +34 -0
- package/dist/src/discover.js +133 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +12 -0
- package/dist/src/internal/environment.d.ts +14 -0
- package/dist/src/internal/environment.js +15 -0
- package/dist/src/packaging/bazel.d.ts +27 -0
- package/dist/src/packaging/bazel.js +125 -0
- package/dist/src/packaging/cargo.d.ts +32 -0
- package/dist/src/packaging/cargo.js +270 -0
- package/dist/src/packaging/cocoapods.d.ts +26 -0
- package/dist/src/packaging/cocoapods.js +187 -0
- package/dist/src/packaging/composer.d.ts +26 -0
- package/dist/src/packaging/composer.js +144 -0
- package/dist/src/packaging/conan.d.ts +30 -0
- package/dist/src/packaging/conan.js +238 -0
- package/dist/src/packaging/conda.d.ts +26 -0
- package/dist/src/packaging/conda.js +320 -0
- package/dist/src/packaging/cpan.d.ts +29 -0
- package/dist/src/packaging/cpan.js +189 -0
- package/dist/src/packaging/cran.d.ts +29 -0
- package/dist/src/packaging/cran.js +176 -0
- package/dist/src/packaging/detect.d.ts +16 -0
- package/dist/src/packaging/detect.js +25 -0
- package/dist/src/packaging/detected-package.d.ts +14 -0
- package/dist/src/packaging/detected-package.js +20 -0
- package/dist/src/packaging/docker.d.ts +27 -0
- package/dist/src/packaging/docker.js +256 -0
- package/dist/src/packaging/gem.d.ts +26 -0
- package/dist/src/packaging/gem.js +234 -0
- package/dist/src/packaging/golang.d.ts +26 -0
- package/dist/src/packaging/golang.js +169 -0
- package/dist/src/packaging/hackage.d.ts +26 -0
- package/dist/src/packaging/hackage.js +305 -0
- package/dist/src/packaging/hex.d.ts +28 -0
- package/dist/src/packaging/hex.js +186 -0
- package/dist/src/packaging/huggingface.d.ts +20 -0
- package/dist/src/packaging/huggingface.js +112 -0
- package/dist/src/packaging/index.d.ts +44 -0
- package/dist/src/packaging/index.js +120 -0
- package/dist/src/packaging/jsr.d.ts +30 -0
- package/dist/src/packaging/jsr.js +227 -0
- package/dist/src/packaging/julia.d.ts +27 -0
- package/dist/src/packaging/julia.js +165 -0
- package/dist/src/packaging/luarocks.d.ts +28 -0
- package/dist/src/packaging/luarocks.js +159 -0
- package/dist/src/packaging/maven.d.ts +27 -0
- package/dist/src/packaging/maven.js +471 -0
- package/dist/src/packaging/mojo.d.ts +29 -0
- package/dist/src/packaging/mojo.js +148 -0
- package/dist/src/packaging/npm.d.ts +26 -0
- package/dist/src/packaging/npm.js +190 -0
- package/dist/src/packaging/nuget.d.ts +26 -0
- package/dist/src/packaging/nuget.js +240 -0
- package/dist/src/packaging/opam.d.ts +27 -0
- package/dist/src/packaging/opam.js +287 -0
- package/dist/src/packaging/pub.d.ts +26 -0
- package/dist/src/packaging/pub.js +357 -0
- package/dist/src/packaging/pypi.d.ts +23 -0
- package/dist/src/packaging/pypi.js +448 -0
- package/dist/src/packaging/read.d.ts +20 -0
- package/dist/src/packaging/read.js +31 -0
- package/dist/src/packaging/reader-io.d.ts +29 -0
- package/dist/src/packaging/reader-io.js +29 -0
- package/dist/src/packaging/swift.d.ts +26 -0
- package/dist/src/packaging/swift.js +141 -0
- package/dist/src/packaging/types.d.ts +43 -0
- package/dist/src/packaging/types.js +8 -0
- package/dist/src/packaging/zig.d.ts +32 -0
- package/dist/src/packaging/zig.js +151 -0
- package/package.json +54 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pypi package detector and reader for package-compatibility discovery.
|
|
3
|
+
*
|
|
4
|
+
* Parses Python dependency files (pyproject.toml, requirements.txt,
|
|
5
|
+
* setup.cfg, Pipfile) and reads axm metadata from installed packages.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as FileSystem from "effect/FileSystem";
|
|
12
|
+
import * as Option from "effect/Option";
|
|
13
|
+
import * as Path from "effect/Path";
|
|
14
|
+
import * as Result from "effect/Result";
|
|
15
|
+
import * as Schema from "effect/Schema";
|
|
16
|
+
import { extractTomlQuotedStrings } from "@agentxm/extension-workspace";
|
|
17
|
+
import { readEnv } from "../internal/environment.js";
|
|
18
|
+
import { PackageTypeSchema } from "@agentxm/extension-model/unstable/packaging/package-type";
|
|
19
|
+
import { decodeAxmMeta, parseJsonOptional, readFileOptional } from "./reader-io.js";
|
|
20
|
+
const pypiType = Schema.decodeUnknownSync(PackageTypeSchema)("pypi");
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Name normalization
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a Python package name per purl spec:
|
|
26
|
+
* lowercase, replace underscores/dots/runs-of-dashes with single dash.
|
|
27
|
+
*/
|
|
28
|
+
const normalizeName = (name) => name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Version extraction
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* Parse a PEP 440 version specifier string and return the version
|
|
34
|
+
* only if it is an exact pin without wildcards.
|
|
35
|
+
*/
|
|
36
|
+
const extractExactVersion = (specifier) => {
|
|
37
|
+
const trimmed = specifier.trim();
|
|
38
|
+
if (trimmed === "")
|
|
39
|
+
return undefined;
|
|
40
|
+
// Only exact pins: ==X.Y.Z (no wildcard)
|
|
41
|
+
const exactMatch = /^==([^*,!~<>=]+)$/.exec(trimmed);
|
|
42
|
+
if (exactMatch?.[1] !== undefined)
|
|
43
|
+
return exactMatch[1].trim();
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Parse a dependency line into name and optional version.
|
|
48
|
+
* Handles: `name`, `name[extras]`, `name>=1.0`, `name==1.0.0`, etc.
|
|
49
|
+
*/
|
|
50
|
+
const parseDependencyLine = (line) => {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
53
|
+
return undefined;
|
|
54
|
+
// Match package name (possibly with extras), then optional version specifiers
|
|
55
|
+
// Package names: letters, digits, hyphens, underscores, dots
|
|
56
|
+
const match = /^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]|[A-Za-z0-9])(?:\[.*?\])?\s*(.*)$/.exec(trimmed);
|
|
57
|
+
if (!match)
|
|
58
|
+
return undefined;
|
|
59
|
+
const rawName = match[1] ?? "";
|
|
60
|
+
const versionPart = match[2]?.trim().replace(/;.*$/, "").trim() ?? ""; // strip environment markers
|
|
61
|
+
return {
|
|
62
|
+
name: normalizeName(rawName),
|
|
63
|
+
version: extractExactVersion(versionPart),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// File parsers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Parse pyproject.toml for dependencies.
|
|
71
|
+
* Uses simple regex parsing for the subset of TOML we need.
|
|
72
|
+
* Returns an Effect so we can log warnings for malformed content.
|
|
73
|
+
*/
|
|
74
|
+
const parsePyprojectToml = (content, source) => Effect.gen(function* () {
|
|
75
|
+
const result = yield* Effect.try({
|
|
76
|
+
try: () => {
|
|
77
|
+
const packages = [];
|
|
78
|
+
// Parse [project] dependencies = [...]
|
|
79
|
+
const projectDepsMatch = /\[project\]\s*\n(?:(?!\[).*\n)*?dependencies\s*=\s*\[([\s\S]*?)\]/m.exec(content);
|
|
80
|
+
if (projectDepsMatch) {
|
|
81
|
+
const depsStr = projectDepsMatch[1] ?? "";
|
|
82
|
+
for (const dep of extractTomlQuotedStrings(depsStr)) {
|
|
83
|
+
const parsed = parseDependencyLine(dep);
|
|
84
|
+
if (parsed) {
|
|
85
|
+
packages.push(makeDetectedPackage(parsed.name, parsed.version, source));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Parse [project.optional-dependencies] groups
|
|
90
|
+
const optDepsRegex = /\[project\.optional-dependencies\]\s*\n([\s\S]*?)(?=\n\[|\n*$)/gm;
|
|
91
|
+
let optMatch;
|
|
92
|
+
while ((optMatch = optDepsRegex.exec(content)) !== null) {
|
|
93
|
+
const section = optMatch[1] ?? "";
|
|
94
|
+
// Match group_name = [...]
|
|
95
|
+
const groupRegex = /\w+\s*=\s*\[([\s\S]*?)\]/gm;
|
|
96
|
+
let groupMatch;
|
|
97
|
+
while ((groupMatch = groupRegex.exec(section)) !== null) {
|
|
98
|
+
for (const dep of extractTomlQuotedStrings(groupMatch[1] ?? "")) {
|
|
99
|
+
const parsed = parseDependencyLine(dep);
|
|
100
|
+
if (parsed) {
|
|
101
|
+
packages.push(makeDetectedPackage(parsed.name, parsed.version, source));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return packages;
|
|
107
|
+
},
|
|
108
|
+
catch: () => ({ _tag: "PyprojectParseError" }),
|
|
109
|
+
}).pipe(Effect.option);
|
|
110
|
+
if (Option.isNone(result)) {
|
|
111
|
+
yield* Effect.logWarning("Malformed pyproject.toml, skipping");
|
|
112
|
+
const empty = [];
|
|
113
|
+
return empty;
|
|
114
|
+
}
|
|
115
|
+
return result.value;
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Parse requirements.txt, following -r includes.
|
|
119
|
+
*/
|
|
120
|
+
const parseRequirementsTxt = (projectDir, content, source, visited) => Effect.gen(function* () {
|
|
121
|
+
const path = yield* Path.Path;
|
|
122
|
+
const packages = [];
|
|
123
|
+
const lines = content.split("\n");
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
// Skip comments and blank lines
|
|
127
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
128
|
+
continue;
|
|
129
|
+
// Handle -r include directives
|
|
130
|
+
const includeMatch = /^-r\s+(.+)$/.exec(trimmed);
|
|
131
|
+
if (includeMatch) {
|
|
132
|
+
const includeFile = (includeMatch[1] ?? "").trim();
|
|
133
|
+
const includePath = path.join(projectDir, includeFile);
|
|
134
|
+
if (!visited.has(includePath)) {
|
|
135
|
+
visited.add(includePath);
|
|
136
|
+
const includeContent = yield* readFileOptional(includePath);
|
|
137
|
+
if (Option.isSome(includeContent)) {
|
|
138
|
+
const included = yield* parseRequirementsTxt(projectDir, includeContent.value, includeFile, visited);
|
|
139
|
+
packages.push(...included);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
yield* Effect.logWarning(`requirements.txt: -r target not found: ${includeFile}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Skip other pip options (-e, -i, --index-url, etc.)
|
|
148
|
+
if (trimmed.startsWith("-"))
|
|
149
|
+
continue;
|
|
150
|
+
const parsed = parseDependencyLine(trimmed);
|
|
151
|
+
if (parsed) {
|
|
152
|
+
packages.push(makeDetectedPackage(parsed.name, parsed.version, source));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return packages;
|
|
156
|
+
});
|
|
157
|
+
/**
|
|
158
|
+
* Parse setup.cfg [options] install_requires.
|
|
159
|
+
* INI format: section headers in brackets, continuation lines are indented.
|
|
160
|
+
*/
|
|
161
|
+
const parseSetupCfg = (content, source) => {
|
|
162
|
+
const packages = [];
|
|
163
|
+
const lines = content.split("\n");
|
|
164
|
+
let inOptions = false;
|
|
165
|
+
let inInstallRequires = false;
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const trimmed = line.trim();
|
|
168
|
+
// Section headers
|
|
169
|
+
if (trimmed.startsWith("[")) {
|
|
170
|
+
inOptions = trimmed === "[options]";
|
|
171
|
+
inInstallRequires = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!inOptions)
|
|
175
|
+
continue;
|
|
176
|
+
// Check for install_requires key
|
|
177
|
+
if (/^install_requires\s*=/.test(trimmed)) {
|
|
178
|
+
inInstallRequires = true;
|
|
179
|
+
// Value may be on the same line after =
|
|
180
|
+
const afterEq = trimmed.replace(/^install_requires\s*=\s*/, "").trim();
|
|
181
|
+
if (afterEq !== "") {
|
|
182
|
+
const parsed = parseDependencyLine(afterEq);
|
|
183
|
+
if (parsed) {
|
|
184
|
+
packages.push(makeDetectedPackage(parsed.name, parsed.version, source));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Continuation lines (indented) under install_requires
|
|
190
|
+
if (inInstallRequires) {
|
|
191
|
+
// Non-indented non-empty line means we've left the value
|
|
192
|
+
if (trimmed !== "" && !line.startsWith(" ") && !line.startsWith("\t")) {
|
|
193
|
+
inInstallRequires = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
197
|
+
continue;
|
|
198
|
+
const parsed = parseDependencyLine(trimmed);
|
|
199
|
+
if (parsed) {
|
|
200
|
+
packages.push(makeDetectedPackage(parsed.name, parsed.version, source));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return packages;
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Parse Pipfile [packages] section.
|
|
208
|
+
*/
|
|
209
|
+
const parsePipfile = (content, source) => {
|
|
210
|
+
const packages = [];
|
|
211
|
+
// Find [packages] section start and extract lines until next section header
|
|
212
|
+
const packagesIdx = content.indexOf("[packages]");
|
|
213
|
+
if (packagesIdx === -1)
|
|
214
|
+
return packages;
|
|
215
|
+
const afterHeader = content.slice(packagesIdx + "[packages]".length);
|
|
216
|
+
// Next section starts with \n[ at the beginning of a line
|
|
217
|
+
const nextSectionIdx = afterHeader.search(/\n\[/);
|
|
218
|
+
const sectionContent = nextSectionIdx >= 0 ? afterHeader.slice(0, nextSectionIdx) : afterHeader;
|
|
219
|
+
const lines = sectionContent.split("\n");
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
const trimmed = line.trim();
|
|
222
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
223
|
+
continue;
|
|
224
|
+
// Pipfile format: name = "version_spec" or name = "*" or name = {version = ">=1.0"}
|
|
225
|
+
const kvMatch = /^([A-Za-z0-9][-A-Za-z0-9_.]*)\s*=\s*(.*)$/.exec(trimmed);
|
|
226
|
+
if (!kvMatch)
|
|
227
|
+
continue;
|
|
228
|
+
const rawName = kvMatch[1] ?? "";
|
|
229
|
+
const valuePart = (kvMatch[2] ?? "").trim();
|
|
230
|
+
let version;
|
|
231
|
+
// Simple string value: ">=4.0" or "*"
|
|
232
|
+
const strMatch = /^["']([^"']*)["']$/.exec(valuePart);
|
|
233
|
+
if (strMatch) {
|
|
234
|
+
const spec = strMatch[1] ?? "";
|
|
235
|
+
if (spec !== "*") {
|
|
236
|
+
version = extractExactVersion(spec);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
packages.push(makeDetectedPackage(normalizeName(rawName), version, source));
|
|
240
|
+
}
|
|
241
|
+
return packages;
|
|
242
|
+
};
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Helpers
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
const makeDetectedPackage = (name, version, source) => ({
|
|
247
|
+
purl: {
|
|
248
|
+
type: pypiType,
|
|
249
|
+
name,
|
|
250
|
+
...(version !== undefined ? { version } : {}),
|
|
251
|
+
},
|
|
252
|
+
type: pypiType,
|
|
253
|
+
source,
|
|
254
|
+
});
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Detector
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
/**
|
|
259
|
+
* pypi package detector. Parses Python dependency files in priority order.
|
|
260
|
+
*
|
|
261
|
+
* @experimental This API is unstable and may change without notice.
|
|
262
|
+
*/
|
|
263
|
+
export const pypiDetector = {
|
|
264
|
+
type: pypiType,
|
|
265
|
+
detect: Effect.fn("detect.pypi")(function* (projectDir) {
|
|
266
|
+
const path = yield* Path.Path;
|
|
267
|
+
const allPackages = [];
|
|
268
|
+
const seenNames = new Set();
|
|
269
|
+
const addUnique = (pkgs) => {
|
|
270
|
+
for (const pkg of pkgs) {
|
|
271
|
+
if (!seenNames.has(pkg.purl.name)) {
|
|
272
|
+
seenNames.add(pkg.purl.name);
|
|
273
|
+
allPackages.push(pkg);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
// 1. pyproject.toml
|
|
278
|
+
const pyprojectPath = path.join(projectDir, "pyproject.toml");
|
|
279
|
+
const pyprojectContent = yield* readFileOptional(pyprojectPath);
|
|
280
|
+
if (Option.isSome(pyprojectContent)) {
|
|
281
|
+
const deps = yield* parsePyprojectToml(pyprojectContent.value, "pyproject.toml");
|
|
282
|
+
addUnique(deps);
|
|
283
|
+
}
|
|
284
|
+
// 2. requirements.txt
|
|
285
|
+
const reqPath = path.join(projectDir, "requirements.txt");
|
|
286
|
+
const reqContent = yield* readFileOptional(reqPath);
|
|
287
|
+
if (Option.isSome(reqContent)) {
|
|
288
|
+
const reqPackages = yield* parseRequirementsTxt(projectDir, reqContent.value, "requirements.txt", new Set([reqPath]));
|
|
289
|
+
addUnique(reqPackages);
|
|
290
|
+
}
|
|
291
|
+
// 3. setup.cfg
|
|
292
|
+
const setupCfgPath = path.join(projectDir, "setup.cfg");
|
|
293
|
+
const setupCfgContent = yield* readFileOptional(setupCfgPath);
|
|
294
|
+
if (Option.isSome(setupCfgContent)) {
|
|
295
|
+
addUnique(parseSetupCfg(setupCfgContent.value, "setup.cfg"));
|
|
296
|
+
}
|
|
297
|
+
// 4. Pipfile
|
|
298
|
+
const pipfilePath = path.join(projectDir, "Pipfile");
|
|
299
|
+
const pipfileContent = yield* readFileOptional(pipfilePath);
|
|
300
|
+
if (Option.isSome(pipfileContent)) {
|
|
301
|
+
addUnique(parsePipfile(pipfileContent.value, "Pipfile"));
|
|
302
|
+
}
|
|
303
|
+
return allPackages;
|
|
304
|
+
}, Effect.annotateLogs({ detector: "pypi" })),
|
|
305
|
+
};
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Reader helpers
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
/**
|
|
310
|
+
* Normalize a dist-info directory name for comparison:
|
|
311
|
+
* lowercase, replace underscores/dots/dashes uniformly.
|
|
312
|
+
*/
|
|
313
|
+
const normalizeDistInfoName = (name) => name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
314
|
+
/**
|
|
315
|
+
* Parse an INI-style entry_points.txt and check for [axm] group.
|
|
316
|
+
* Returns the first value from the [axm] group if present.
|
|
317
|
+
*/
|
|
318
|
+
const parseEntryPoints = (content) => {
|
|
319
|
+
const lines = content.split("\n");
|
|
320
|
+
let inAxmGroup = false;
|
|
321
|
+
for (const line of lines) {
|
|
322
|
+
const trimmed = line.trim();
|
|
323
|
+
// Section header
|
|
324
|
+
const sectionMatch = /^\[(.+)\]$/.exec(trimmed);
|
|
325
|
+
if (sectionMatch) {
|
|
326
|
+
inAxmGroup = sectionMatch[1] === "axm";
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (inAxmGroup && trimmed !== "" && !trimmed.startsWith("#")) {
|
|
330
|
+
// key = value format
|
|
331
|
+
const kvMatch = /^(\S+)\s*=\s*(.+)$/.exec(trimmed);
|
|
332
|
+
if (kvMatch) {
|
|
333
|
+
return Option.some((kvMatch[2] ?? "").trim());
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return Option.none();
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* Resolve the site-packages directory to scan.
|
|
341
|
+
* Checks $VIRTUAL_ENV first, then falls back to system.
|
|
342
|
+
*/
|
|
343
|
+
const resolveSitePackages = () => Effect.gen(function* () {
|
|
344
|
+
const fs = yield* FileSystem.FileSystem;
|
|
345
|
+
const path = yield* Path.Path;
|
|
346
|
+
const virtualEnv = yield* Effect.sync(() => readEnv("VIRTUAL_ENV"));
|
|
347
|
+
if (virtualEnv) {
|
|
348
|
+
// Scan for python* directories under lib/
|
|
349
|
+
const libDir = path.join(virtualEnv, "lib");
|
|
350
|
+
const libExists = yield* fs.exists(libDir).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
351
|
+
if (libExists) {
|
|
352
|
+
const entries = yield* fs.readDirectory(libDir).pipe(Effect.catch(() => {
|
|
353
|
+
const empty = [];
|
|
354
|
+
return Effect.succeed(empty);
|
|
355
|
+
}));
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (entry.startsWith("python")) {
|
|
358
|
+
const sp = path.join(libDir, entry, "site-packages");
|
|
359
|
+
const spExists = yield* fs.exists(sp).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
360
|
+
if (spExists)
|
|
361
|
+
return Option.some(sp);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Fallback: no site-packages found
|
|
367
|
+
return Option.none();
|
|
368
|
+
});
|
|
369
|
+
/**
|
|
370
|
+
* Find a .dist-info directory matching the given package name.
|
|
371
|
+
*/
|
|
372
|
+
const findDistInfo = (sitePackages, packageName) => Effect.gen(function* () {
|
|
373
|
+
const fs = yield* FileSystem.FileSystem;
|
|
374
|
+
const path = yield* Path.Path;
|
|
375
|
+
const normalizedTarget = normalizeDistInfoName(packageName);
|
|
376
|
+
const entries = yield* fs.readDirectory(sitePackages).pipe(Effect.catch(() => {
|
|
377
|
+
const empty = [];
|
|
378
|
+
return Effect.succeed(empty);
|
|
379
|
+
}));
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
if (!entry.endsWith(".dist-info"))
|
|
382
|
+
continue;
|
|
383
|
+
// dist-info format: Name-Version.dist-info
|
|
384
|
+
// Extract the name part (everything before the last hyphen-version)
|
|
385
|
+
const withoutSuffix = entry.replace(/\.dist-info$/, "");
|
|
386
|
+
// Split on hyphen that precedes a version (digit)
|
|
387
|
+
const dashIdx = withoutSuffix.search(/-\d/);
|
|
388
|
+
const dirName = dashIdx >= 0 ? withoutSuffix.substring(0, dashIdx) : withoutSuffix;
|
|
389
|
+
if (normalizeDistInfoName(dirName) === normalizedTarget) {
|
|
390
|
+
return Option.some(path.join(sitePackages, entry));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return Option.none();
|
|
394
|
+
});
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Reader
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
/**
|
|
399
|
+
* pypi package reader. Reads axm metadata from installed Python packages.
|
|
400
|
+
*
|
|
401
|
+
* @experimental This API is unstable and may change without notice.
|
|
402
|
+
*/
|
|
403
|
+
export const pypiReader = {
|
|
404
|
+
type: pypiType,
|
|
405
|
+
read: Effect.fn("read.pypi")(function* (pkg) {
|
|
406
|
+
const path = yield* Path.Path;
|
|
407
|
+
// 1. Resolve site-packages
|
|
408
|
+
const sitePackagesOpt = yield* resolveSitePackages();
|
|
409
|
+
if (Option.isNone(sitePackagesOpt))
|
|
410
|
+
return Option.none();
|
|
411
|
+
const sitePackages = sitePackagesOpt.value;
|
|
412
|
+
// 2. Find .dist-info directory
|
|
413
|
+
const distInfoOpt = yield* findDistInfo(sitePackages, pkg.purl.name);
|
|
414
|
+
if (Option.isNone(distInfoOpt))
|
|
415
|
+
return Option.none();
|
|
416
|
+
const distInfoDir = distInfoOpt.value;
|
|
417
|
+
// 3. Read entry_points.txt
|
|
418
|
+
const entryPointsPath = path.join(distInfoDir, "entry_points.txt");
|
|
419
|
+
const entryPointsContent = yield* readFileOptional(entryPointsPath);
|
|
420
|
+
if (Option.isNone(entryPointsContent))
|
|
421
|
+
return Option.none();
|
|
422
|
+
// 4. Check for [axm] group
|
|
423
|
+
const axmEntry = parseEntryPoints(entryPointsContent.value);
|
|
424
|
+
if (Option.isNone(axmEntry))
|
|
425
|
+
return Option.none();
|
|
426
|
+
// 5. Locate axm.json from the entry point value
|
|
427
|
+
// Entry format: "package_module:axm.json" -> look for axm.json in the package dir
|
|
428
|
+
const entryValue = axmEntry.value;
|
|
429
|
+
const colonIdx = entryValue.indexOf(":");
|
|
430
|
+
const modulePart = colonIdx >= 0 ? entryValue.substring(0, colonIdx) : entryValue;
|
|
431
|
+
const filePart = colonIdx >= 0 ? entryValue.substring(colonIdx + 1) : "axm.json";
|
|
432
|
+
const axmJsonPath = path.join(sitePackages, modulePart, filePart);
|
|
433
|
+
const axmJsonContent = yield* readFileOptional(axmJsonPath);
|
|
434
|
+
if (Option.isNone(axmJsonContent))
|
|
435
|
+
return Option.none();
|
|
436
|
+
// 6. Parse and validate axm.json
|
|
437
|
+
const parsed = yield* parseJsonOptional(axmJsonContent.value, `${pkg.purl.name}/axm.json`);
|
|
438
|
+
if (Option.isNone(parsed))
|
|
439
|
+
return Option.none();
|
|
440
|
+
const metaResult = decodeAxmMeta(parsed.value);
|
|
441
|
+
if (Result.isFailure(metaResult)) {
|
|
442
|
+
yield* Effect.logWarning(`Invalid axm metadata in ${pkg.purl.name}: schema validation failed`);
|
|
443
|
+
return Option.none();
|
|
444
|
+
}
|
|
445
|
+
return Option.some(metaResult.success.extensions);
|
|
446
|
+
}, Effect.annotateLogs({ reader: "pypi" })),
|
|
447
|
+
};
|
|
448
|
+
//# sourceMappingURL=pypi.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read orchestrator: matches detected packages to readers and collects
|
|
3
|
+
* recommended extension refs into a HashMap keyed by encoded purl.
|
|
4
|
+
*
|
|
5
|
+
* @experimental This API is unstable and may change without notice.
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as HashMap from "effect/HashMap";
|
|
10
|
+
import type { DetectedPackage, PackageReader } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Read local recommendations for each detected package using matching readers.
|
|
13
|
+
*
|
|
14
|
+
* @experimental This API is unstable and may change without notice.
|
|
15
|
+
*/
|
|
16
|
+
export declare const readLocalRecommendations: (packages: readonly DetectedPackage[], readers: readonly PackageReader[]) => Effect.Effect<HashMap.HashMap<string, readonly {
|
|
17
|
+
readonly ref: string;
|
|
18
|
+
readonly versionRange?: (string & import("effect/Brand").Brand<"VersionRange">) | null | undefined;
|
|
19
|
+
}[]>, never, import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
|
|
20
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read orchestrator: matches detected packages to readers and collects
|
|
3
|
+
* recommended extension refs into a HashMap keyed by encoded purl.
|
|
4
|
+
*
|
|
5
|
+
* @experimental This API is unstable and may change without notice.
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import * as Array from "effect/Array";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as HashMap from "effect/HashMap";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import * as Schema from "effect/Schema";
|
|
13
|
+
import { PackageUrlSchema } from "@agentxm/extension-model/unstable/packaging/package-url";
|
|
14
|
+
const encodePurl = Schema.encodeSync(PackageUrlSchema);
|
|
15
|
+
/**
|
|
16
|
+
* Read local recommendations for each detected package using matching readers.
|
|
17
|
+
*
|
|
18
|
+
* @experimental This API is unstable and may change without notice.
|
|
19
|
+
*/
|
|
20
|
+
export const readLocalRecommendations = Effect.fn("discover.readLocal")(function* (packages, readers) {
|
|
21
|
+
const results = yield* Effect.forEach(packages, (pkg) => {
|
|
22
|
+
const reader = readers.find((r) => r.type === pkg.type);
|
|
23
|
+
if (!reader)
|
|
24
|
+
return Effect.succeed(Option.none());
|
|
25
|
+
return reader
|
|
26
|
+
.read(pkg)
|
|
27
|
+
.pipe(Effect.map((result) => Option.map(result, (refs) => [encodePurl(pkg.purl), refs])));
|
|
28
|
+
}, { concurrency: "unbounded" });
|
|
29
|
+
return HashMap.fromIterable(Array.getSomes(results));
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reader I/O helpers for package-compatibility discovery.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as FileSystem from "effect/FileSystem";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
export declare const decodePurl: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
11
|
+
readonly type: string & import("effect/Brand").Brand<"PackageType">;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly namespace?: string | undefined;
|
|
14
|
+
readonly version?: string | undefined;
|
|
15
|
+
readonly qualifiers?: {
|
|
16
|
+
readonly [x: string]: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
readonly subpath?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
export declare const decodeAxmMeta: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => import("effect/Result").Result<{
|
|
21
|
+
readonly extensions: readonly {
|
|
22
|
+
readonly ref: string;
|
|
23
|
+
readonly versionRange?: (string & import("effect/Brand").Brand<"VersionRange">) | null | undefined;
|
|
24
|
+
}[];
|
|
25
|
+
readonly $schema?: string | undefined;
|
|
26
|
+
}, Schema.SchemaError>;
|
|
27
|
+
export declare const readFileOptional: (filePath: string) => Effect.Effect<Option.Option<string>, never, FileSystem.FileSystem>;
|
|
28
|
+
export declare const parseJsonOptional: (content: string, context: string) => Effect.Effect<Option.Option<unknown>, never, never>;
|
|
29
|
+
//# sourceMappingURL=reader-io.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared reader I/O helpers for package-compatibility discovery.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as FileSystem from "effect/FileSystem";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import { AxmPackageMetaSchema } from "@agentxm/registry-client";
|
|
11
|
+
import { PackageUrlSchema } from "@agentxm/extension-model/unstable/packaging/package-url";
|
|
12
|
+
export const decodePurl = Schema.decodeUnknownSync(PackageUrlSchema);
|
|
13
|
+
export const decodeAxmMeta = Schema.decodeUnknownResult(AxmPackageMetaSchema);
|
|
14
|
+
export const readFileOptional = (filePath) => Effect.gen(function* () {
|
|
15
|
+
const fs = yield* FileSystem.FileSystem;
|
|
16
|
+
return yield* fs.readFileString(filePath).pipe(Effect.option);
|
|
17
|
+
});
|
|
18
|
+
export const parseJsonOptional = (content, context) => Effect.gen(function* () {
|
|
19
|
+
const result = yield* Effect.try({
|
|
20
|
+
try: () => JSON.parse(content),
|
|
21
|
+
catch: () => ({ _tag: "JsonParseError" }),
|
|
22
|
+
}).pipe(Effect.option);
|
|
23
|
+
if (Option.isNone(result)) {
|
|
24
|
+
yield* Effect.logWarning(`Malformed JSON in ${context}, skipping`);
|
|
25
|
+
return Option.none();
|
|
26
|
+
}
|
|
27
|
+
return Option.some(result.value);
|
|
28
|
+
});
|
|
29
|
+
//# sourceMappingURL=reader-io.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift package detector and reader for package-compatibility discovery.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
import type { PackageDetector, PackageReader } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Swift package detector.
|
|
10
|
+
*
|
|
11
|
+
* Scans `Package.swift` in the project directory and extracts `.package(url: ...)`
|
|
12
|
+
* dependencies using regex pattern matching.
|
|
13
|
+
*
|
|
14
|
+
* @experimental This API is unstable and may change without notice.
|
|
15
|
+
*/
|
|
16
|
+
export declare const swiftDetector: PackageDetector;
|
|
17
|
+
/**
|
|
18
|
+
* Swift package reader.
|
|
19
|
+
*
|
|
20
|
+
* Reads `.build/checkouts/<package-name>/axm.json` for each detected SwiftPM
|
|
21
|
+
* package and extracts recommendation metadata.
|
|
22
|
+
*
|
|
23
|
+
* @experimental This API is unstable and may change without notice.
|
|
24
|
+
*/
|
|
25
|
+
export declare const swiftReader: PackageReader;
|
|
26
|
+
//# sourceMappingURL=swift.d.ts.map
|