@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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker image detector and reader for package-compatibility discovery.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Path from "effect/Path";
|
|
10
|
+
import * as Result from "effect/Result";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
12
|
+
import { PackageURL } from "packageurl-js";
|
|
13
|
+
import { PackageTypeSchema } from "@agentxm/extension-model/unstable/packaging/package-type";
|
|
14
|
+
import { decodeAxmMeta, decodePurl, parseJsonOptional, readFileOptional } from "./reader-io.js";
|
|
15
|
+
const dockerType = Schema.decodeUnknownSync(PackageTypeSchema)("docker");
|
|
16
|
+
/**
|
|
17
|
+
* Parse a Docker image reference into namespace, name, and version components.
|
|
18
|
+
*
|
|
19
|
+
* Image references can be:
|
|
20
|
+
* - `name` (e.g. `nginx`)
|
|
21
|
+
* - `name:tag` (e.g. `nginx:alpine`)
|
|
22
|
+
* - `org/name:tag` (e.g. `library/nginx:1.25`)
|
|
23
|
+
* - `registry/org/name:tag` (e.g. `ghcr.io/org/myapp:latest`)
|
|
24
|
+
*/
|
|
25
|
+
const parseImageRef = (ref) => {
|
|
26
|
+
const trimmed = ref.trim();
|
|
27
|
+
if (trimmed.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
// Split on : to get name part and tag part
|
|
30
|
+
const colonIdx = trimmed.lastIndexOf(":");
|
|
31
|
+
let namePart;
|
|
32
|
+
let tag;
|
|
33
|
+
if (colonIdx > 0 && !trimmed.slice(colonIdx).includes("/")) {
|
|
34
|
+
namePart = trimmed.slice(0, colonIdx);
|
|
35
|
+
tag = trimmed.slice(colonIdx + 1);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
namePart = trimmed;
|
|
39
|
+
tag = undefined;
|
|
40
|
+
}
|
|
41
|
+
// Remove @sha256 digest if present
|
|
42
|
+
const digestIdx = namePart.indexOf("@");
|
|
43
|
+
if (digestIdx > 0) {
|
|
44
|
+
namePart = namePart.slice(0, digestIdx);
|
|
45
|
+
}
|
|
46
|
+
// Split namespace and name
|
|
47
|
+
const lastSlash = namePart.lastIndexOf("/");
|
|
48
|
+
let namespace;
|
|
49
|
+
let name;
|
|
50
|
+
if (lastSlash > 0) {
|
|
51
|
+
namespace = namePart.slice(0, lastSlash);
|
|
52
|
+
name = namePart.slice(lastSlash + 1);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
namespace = undefined;
|
|
56
|
+
name = namePart;
|
|
57
|
+
}
|
|
58
|
+
if (name.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
// `latest` tag or no tag → versionless
|
|
61
|
+
const version = tag !== undefined && tag !== "latest" && tag.length > 0 ? tag : undefined;
|
|
62
|
+
return { namespace, name, version };
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Convert a parsed image reference to a DetectedPackage.
|
|
66
|
+
*/
|
|
67
|
+
const imageToPurl = (ref, source) => {
|
|
68
|
+
const parsed = parseImageRef(ref);
|
|
69
|
+
if (parsed === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
const purl = new PackageURL("docker", parsed.namespace ?? null, parsed.name, parsed.version ?? null, null, null);
|
|
72
|
+
const purlParts = decodePurl(purl.toString());
|
|
73
|
+
return { purl: purlParts, type: dockerType, source };
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Parse ARG directives from Dockerfile content.
|
|
77
|
+
* Returns a map of variable name to default value (or undefined if no default).
|
|
78
|
+
*/
|
|
79
|
+
const parseDockerArgs = (content) => {
|
|
80
|
+
const args = new Map();
|
|
81
|
+
const lines = content.split("\n");
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed.toUpperCase().startsWith("ARG "))
|
|
85
|
+
continue;
|
|
86
|
+
const rest = trimmed.slice(4).trim();
|
|
87
|
+
const eqIdx = rest.indexOf("=");
|
|
88
|
+
if (eqIdx > 0) {
|
|
89
|
+
const name = rest.slice(0, eqIdx).trim();
|
|
90
|
+
const value = rest.slice(eqIdx + 1).trim();
|
|
91
|
+
args.set(name, value);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
args.set(rest, undefined);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return args;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a variable reference like `${VARIABLE}` using ARG defaults.
|
|
101
|
+
* Returns the resolved string, or undefined if unresolvable.
|
|
102
|
+
*/
|
|
103
|
+
const resolveVariable = (imageRef, args) => {
|
|
104
|
+
// Check if the entire reference or parts contain variables
|
|
105
|
+
const varPattern = /\$\{([^}]+)\}/g;
|
|
106
|
+
let resolved = imageRef;
|
|
107
|
+
let match = varPattern.exec(imageRef);
|
|
108
|
+
while (match !== null) {
|
|
109
|
+
const varName = match[1];
|
|
110
|
+
if (varName === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
// Check for ${VAR:-default} syntax
|
|
113
|
+
const colonIdx = varName.indexOf(":-");
|
|
114
|
+
const defaultValue = colonIdx > 0 ? varName.slice(colonIdx + 2) : args.get(varName);
|
|
115
|
+
if (defaultValue === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
resolved = resolved.replace(match[0], defaultValue);
|
|
118
|
+
match = varPattern.exec(imageRef);
|
|
119
|
+
}
|
|
120
|
+
return resolved;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Parse FROM directives from Dockerfile content.
|
|
124
|
+
*/
|
|
125
|
+
const parseDockerfile = (content, source) => {
|
|
126
|
+
const args = parseDockerArgs(content);
|
|
127
|
+
const lines = content.split("\n");
|
|
128
|
+
const results = [];
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed.toUpperCase().startsWith("FROM "))
|
|
132
|
+
continue;
|
|
133
|
+
const rest = trimmed.slice(5).trim();
|
|
134
|
+
// Remove AS alias
|
|
135
|
+
const parts = rest.split(/\s+/);
|
|
136
|
+
let imageRef = parts[0];
|
|
137
|
+
if (imageRef === undefined || imageRef.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
// Resolve variables
|
|
140
|
+
if (imageRef.includes("${")) {
|
|
141
|
+
const resolved = resolveVariable(imageRef, args);
|
|
142
|
+
if (resolved === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
imageRef = resolved;
|
|
145
|
+
}
|
|
146
|
+
// Skip scratch
|
|
147
|
+
if (imageRef === "scratch")
|
|
148
|
+
continue;
|
|
149
|
+
const detected = imageToPurl(imageRef, source);
|
|
150
|
+
if (detected !== undefined)
|
|
151
|
+
results.push(detected);
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Parse docker-compose YAML using line-based parsing.
|
|
157
|
+
* Extracts `image:` values from service definitions.
|
|
158
|
+
*/
|
|
159
|
+
const parseDockerCompose = (content, source) => {
|
|
160
|
+
const lines = content.split("\n");
|
|
161
|
+
const results = [];
|
|
162
|
+
for (const line of lines) {
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
if (trimmed.startsWith("#"))
|
|
165
|
+
continue;
|
|
166
|
+
// Match `image: value` lines
|
|
167
|
+
if (trimmed.startsWith("image:")) {
|
|
168
|
+
const value = trimmed.slice(6).trim();
|
|
169
|
+
// Remove quotes if present
|
|
170
|
+
const cleaned = value.replace(/^["']|["']$/g, "");
|
|
171
|
+
if (cleaned.length > 0) {
|
|
172
|
+
const detected = imageToPurl(cleaned, source);
|
|
173
|
+
if (detected !== undefined)
|
|
174
|
+
results.push(detected);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return results;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Docker package detector.
|
|
182
|
+
*
|
|
183
|
+
* Scans `Dockerfile`, `docker-compose.yml`, and `docker-compose.yaml`
|
|
184
|
+
* for image dependencies.
|
|
185
|
+
*
|
|
186
|
+
* @experimental This API is unstable and may change without notice.
|
|
187
|
+
*/
|
|
188
|
+
export const dockerDetector = {
|
|
189
|
+
type: dockerType,
|
|
190
|
+
detect: Effect.fn("detect.docker")(function* (projectDir) {
|
|
191
|
+
const path = yield* Path.Path;
|
|
192
|
+
const results = [];
|
|
193
|
+
// Parse Dockerfile
|
|
194
|
+
const dockerfilePath = path.join(projectDir, "Dockerfile");
|
|
195
|
+
const dockerfileContent = yield* readFileOptional(dockerfilePath);
|
|
196
|
+
if (Option.isSome(dockerfileContent)) {
|
|
197
|
+
const deps = parseDockerfile(dockerfileContent.value, dockerfilePath);
|
|
198
|
+
results.push(...deps);
|
|
199
|
+
}
|
|
200
|
+
// Parse docker-compose.yml
|
|
201
|
+
const composeYmlPath = path.join(projectDir, "docker-compose.yml");
|
|
202
|
+
const composeYmlContent = yield* readFileOptional(composeYmlPath);
|
|
203
|
+
if (Option.isSome(composeYmlContent)) {
|
|
204
|
+
const deps = parseDockerCompose(composeYmlContent.value, composeYmlPath);
|
|
205
|
+
results.push(...deps);
|
|
206
|
+
}
|
|
207
|
+
// Parse docker-compose.yaml
|
|
208
|
+
const composeYamlPath = path.join(projectDir, "docker-compose.yaml");
|
|
209
|
+
const composeYamlContent = yield* readFileOptional(composeYamlPath);
|
|
210
|
+
if (Option.isSome(composeYamlContent)) {
|
|
211
|
+
const deps = parseDockerCompose(composeYamlContent.value, composeYamlPath);
|
|
212
|
+
results.push(...deps);
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}, Effect.annotateLogs({ detector: "docker" }), Effect.withSpan("detect.docker")),
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Docker package reader.
|
|
219
|
+
*
|
|
220
|
+
* Inspects local Docker image metadata for `sh.axm.recommended-extensions`
|
|
221
|
+
* annotation. Since inspecting Docker images requires the Docker CLI,
|
|
222
|
+
* this reader returns Option.none when Docker is not available.
|
|
223
|
+
*
|
|
224
|
+
* @experimental This API is unstable and may change without notice.
|
|
225
|
+
*/
|
|
226
|
+
export const dockerReader = {
|
|
227
|
+
type: dockerType,
|
|
228
|
+
read: Effect.fn("read.docker")(function* (pkg) {
|
|
229
|
+
const path = yield* Path.Path;
|
|
230
|
+
// Reconstruct the image name from purl parts
|
|
231
|
+
const imageName = pkg.purl.namespace
|
|
232
|
+
? `${pkg.purl.namespace}/${pkg.purl.name}`
|
|
233
|
+
: pkg.purl.name;
|
|
234
|
+
const imageRef = pkg.purl.version ? `${imageName}:${pkg.purl.version}` : imageName;
|
|
235
|
+
// Derive project directory from source
|
|
236
|
+
const projectDir = path.dirname(pkg.source);
|
|
237
|
+
// Check for a local .axm-docker-annotations/<image>.json file
|
|
238
|
+
// This is a simplified approach that checks for sidecar annotation files
|
|
239
|
+
const safeImageName = imageRef.replace(/[/:]/g, "_");
|
|
240
|
+
const annotationPath = path.join(projectDir, ".axm-docker-annotations", `${safeImageName}.json`);
|
|
241
|
+
const content = yield* readFileOptional(annotationPath);
|
|
242
|
+
if (Option.isNone(content))
|
|
243
|
+
return Option.none();
|
|
244
|
+
const parsed = yield* parseJsonOptional(content.value, `docker annotations for ${imageRef}`);
|
|
245
|
+
if (Option.isNone(parsed))
|
|
246
|
+
return Option.none();
|
|
247
|
+
// Validate axm metadata structure
|
|
248
|
+
const metaResult = decodeAxmMeta(parsed.value);
|
|
249
|
+
if (Result.isFailure(metaResult)) {
|
|
250
|
+
yield* Effect.logWarning(`Invalid axm metadata in docker image ${imageRef}: schema validation failed`);
|
|
251
|
+
return Option.none();
|
|
252
|
+
}
|
|
253
|
+
return Option.some(metaResult.success.extensions);
|
|
254
|
+
}, Effect.annotateLogs({ reader: "docker" }), Effect.withSpan("read.docker")),
|
|
255
|
+
};
|
|
256
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby gem 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
|
+
* Gem package detector.
|
|
10
|
+
*
|
|
11
|
+
* Scans `Gemfile` and `*.gemspec` files in the project directory and extracts
|
|
12
|
+
* gem dependencies. Deduplicates gems that appear in both sources.
|
|
13
|
+
*
|
|
14
|
+
* @experimental This API is unstable and may change without notice.
|
|
15
|
+
*/
|
|
16
|
+
export declare const gemDetector: PackageDetector;
|
|
17
|
+
/**
|
|
18
|
+
* Gem package reader.
|
|
19
|
+
*
|
|
20
|
+
* Reads gemspec metadata from `<gem-dir>/specifications/<gem>.gemspec`
|
|
21
|
+
* and extracts `axm_`-prefixed metadata keys.
|
|
22
|
+
*
|
|
23
|
+
* @experimental This API is unstable and may change without notice.
|
|
24
|
+
*/
|
|
25
|
+
export declare const gemReader: PackageReader;
|
|
26
|
+
//# sourceMappingURL=gem.d.ts.map
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby gem package detector and reader for package-compatibility discovery.
|
|
3
|
+
*
|
|
4
|
+
* @experimental This API is unstable and may change without notice.
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
// Intentional escape hatch: node:os homedir() has no @effect/platform equivalent.
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as FileSystem from "effect/FileSystem";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import * as Path from "effect/Path";
|
|
13
|
+
import * as Result from "effect/Result";
|
|
14
|
+
import * as Schema from "effect/Schema";
|
|
15
|
+
import { PackageURL } from "packageurl-js";
|
|
16
|
+
import { envWithDefault } from "../internal/environment.js";
|
|
17
|
+
import { PackageTypeSchema } from "@agentxm/extension-model/unstable/packaging/package-type";
|
|
18
|
+
import { decodeAxmMeta, decodePurl, readFileOptional } from "./reader-io.js";
|
|
19
|
+
const gemType = Schema.decodeUnknownSync(PackageTypeSchema)("gem");
|
|
20
|
+
/** Range operators that indicate non-exact version constraints. */
|
|
21
|
+
const RANGE_OPERATORS = ["~>", ">=", "<=", ">", "<", "!="];
|
|
22
|
+
/**
|
|
23
|
+
* Returns true if the version specifier is an exact version (no range operators).
|
|
24
|
+
* Exact versions are bare numeric strings like "5.6.7".
|
|
25
|
+
*/
|
|
26
|
+
const isExactVersion = (specifier) => {
|
|
27
|
+
const trimmed = specifier.trim();
|
|
28
|
+
if (RANGE_OPERATORS.some((op) => trimmed.startsWith(op)))
|
|
29
|
+
return false;
|
|
30
|
+
return /^\d+(\.\d+)*$/.test(trimmed);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Regex to match `gem 'name'` or `gem "name"` directives in a Gemfile.
|
|
34
|
+
* Captures: gem name, optional version string, optional options.
|
|
35
|
+
*/
|
|
36
|
+
const GEMFILE_GEM_RE = /^\s*gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]*)['"]\s*)?(?:,\s*(.*))?$/;
|
|
37
|
+
/** Patterns for path/git/github options that indicate non-registry sources. */
|
|
38
|
+
const SKIP_OPTIONS_RE = /\b(?:path|git|github)\s*:/;
|
|
39
|
+
/**
|
|
40
|
+
* Parse a Gemfile line into a gem name and optional version, or undefined if skipped.
|
|
41
|
+
*/
|
|
42
|
+
const parseGemfileLine = (line, source) => {
|
|
43
|
+
const match = GEMFILE_GEM_RE.exec(line);
|
|
44
|
+
if (match === null)
|
|
45
|
+
return undefined;
|
|
46
|
+
const name = match[1];
|
|
47
|
+
const versionStr = match[2];
|
|
48
|
+
const options = match[3];
|
|
49
|
+
if (name === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
// Check for path/git/github options - skip non-registry sources
|
|
52
|
+
if (options !== undefined && SKIP_OPTIONS_RE.test(options))
|
|
53
|
+
return undefined;
|
|
54
|
+
// Also check the full line for options that might come after the version
|
|
55
|
+
const afterName = line.slice(line.indexOf(name) + name.length);
|
|
56
|
+
if (SKIP_OPTIONS_RE.test(afterName))
|
|
57
|
+
return undefined;
|
|
58
|
+
const version = versionStr !== undefined && isExactVersion(versionStr) ? versionStr : undefined;
|
|
59
|
+
const purl = new PackageURL("gem", null, name, version ?? null, null, null);
|
|
60
|
+
const purlParts = decodePurl(purl.toString());
|
|
61
|
+
return { purl: purlParts, type: gemType, source };
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Parse Gemfile content and extract gem dependencies.
|
|
65
|
+
*/
|
|
66
|
+
const parseGemfile = (content, source) => {
|
|
67
|
+
const lines = content.split("\n");
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
72
|
+
continue;
|
|
73
|
+
const detected = parseGemfileLine(trimmed, source);
|
|
74
|
+
if (detected !== undefined)
|
|
75
|
+
results.push(detected);
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Regex to match add_dependency, add_runtime_dependency, add_development_dependency
|
|
81
|
+
* in gemspec files.
|
|
82
|
+
*/
|
|
83
|
+
const GEMSPEC_DEP_RE = /\.\s*add_(?:runtime_|development_)?dependency\s*\(?\s*['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]*)['"]\s*)?/;
|
|
84
|
+
/**
|
|
85
|
+
* Parse a gemspec file and extract dependency declarations.
|
|
86
|
+
*/
|
|
87
|
+
const parseGemspec = (content, source) => {
|
|
88
|
+
const lines = content.split("\n");
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const match = GEMSPEC_DEP_RE.exec(line);
|
|
92
|
+
if (match === null)
|
|
93
|
+
continue;
|
|
94
|
+
const name = match[1];
|
|
95
|
+
if (name === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const versionStr = match[2];
|
|
98
|
+
const version = versionStr !== undefined && isExactVersion(versionStr) ? versionStr : undefined;
|
|
99
|
+
const purl = new PackageURL("gem", null, name, version ?? null, null, null);
|
|
100
|
+
const purlParts = decodePurl(purl.toString());
|
|
101
|
+
results.push({ purl: purlParts, type: gemType, source });
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Gem package detector.
|
|
107
|
+
*
|
|
108
|
+
* Scans `Gemfile` and `*.gemspec` files in the project directory and extracts
|
|
109
|
+
* gem dependencies. Deduplicates gems that appear in both sources.
|
|
110
|
+
*
|
|
111
|
+
* @experimental This API is unstable and may change without notice.
|
|
112
|
+
*/
|
|
113
|
+
export const gemDetector = {
|
|
114
|
+
type: gemType,
|
|
115
|
+
detect: Effect.fn("detect.gem")(function* (projectDir) {
|
|
116
|
+
const fs = yield* FileSystem.FileSystem;
|
|
117
|
+
const path = yield* Path.Path;
|
|
118
|
+
const results = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const addUnique = (deps) => {
|
|
121
|
+
for (const dep of deps) {
|
|
122
|
+
const key = dep.purl.name;
|
|
123
|
+
if (!seen.has(key)) {
|
|
124
|
+
seen.add(key);
|
|
125
|
+
results.push(dep);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// Parse Gemfile
|
|
130
|
+
const gemfilePath = path.join(projectDir, "Gemfile");
|
|
131
|
+
const gemfileContent = yield* readFileOptional(gemfilePath);
|
|
132
|
+
if (Option.isSome(gemfileContent)) {
|
|
133
|
+
const deps = parseGemfile(gemfileContent.value, gemfilePath);
|
|
134
|
+
addUnique(deps);
|
|
135
|
+
}
|
|
136
|
+
// Parse *.gemspec files
|
|
137
|
+
const dirEntries = yield* fs.readDirectory(projectDir).pipe(Effect.option);
|
|
138
|
+
if (Option.isSome(dirEntries)) {
|
|
139
|
+
for (const entry of dirEntries.value) {
|
|
140
|
+
if (entry.endsWith(".gemspec")) {
|
|
141
|
+
const gemspecPath = path.join(projectDir, entry);
|
|
142
|
+
const gemspecContent = yield* readFileOptional(gemspecPath);
|
|
143
|
+
if (Option.isSome(gemspecContent)) {
|
|
144
|
+
const deps = parseGemspec(gemspecContent.value, gemspecPath);
|
|
145
|
+
addUnique(deps);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}, Effect.annotateLogs({ detector: "gem" }), Effect.withSpan("detect.gem")),
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the GEM_HOME or default gem installation directory.
|
|
155
|
+
* Uses GEM_HOME env var, falling back to the default Ruby gems directory.
|
|
156
|
+
*/
|
|
157
|
+
const resolveGemDir = () => envWithDefault("GEM_HOME", `${os.homedir()}/.gem/ruby`);
|
|
158
|
+
/**
|
|
159
|
+
* Parse the `axm_extensions` value from a gemspec metadata string.
|
|
160
|
+
* The value format is a JSON-stringified array of extension declaration objects.
|
|
161
|
+
*/
|
|
162
|
+
const parseAxmMetadataFromGemspec = (content) => {
|
|
163
|
+
const match = /"axm_extensions"\s*=>\s*"((?:\\.|[^"\\])*)"/.exec(content);
|
|
164
|
+
if (match === null || match[1] === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
const rawValue = match[1];
|
|
167
|
+
try {
|
|
168
|
+
const decodedValue = JSON.parse(`"${rawValue}"`);
|
|
169
|
+
if (typeof decodedValue !== "string")
|
|
170
|
+
return undefined;
|
|
171
|
+
return { extensions: JSON.parse(decodedValue) };
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Gem package reader.
|
|
179
|
+
*
|
|
180
|
+
* Reads gemspec metadata from `<gem-dir>/specifications/<gem>.gemspec`
|
|
181
|
+
* and extracts `axm_`-prefixed metadata keys.
|
|
182
|
+
*
|
|
183
|
+
* @experimental This API is unstable and may change without notice.
|
|
184
|
+
*/
|
|
185
|
+
export const gemReader = {
|
|
186
|
+
type: gemType,
|
|
187
|
+
read: Effect.fn("read.gem")(function* (pkg) {
|
|
188
|
+
const fs = yield* FileSystem.FileSystem;
|
|
189
|
+
const path = yield* Path.Path;
|
|
190
|
+
const gemDir = yield* resolveGemDir();
|
|
191
|
+
const gemName = pkg.purl.name;
|
|
192
|
+
const version = pkg.purl.version;
|
|
193
|
+
// Try to find the gemspec in the specifications directory.
|
|
194
|
+
// Gemspec filename format: <name>-<version>.gemspec
|
|
195
|
+
const specsDir = path.join(gemDir, "specifications");
|
|
196
|
+
// If no version, try to find any matching gemspec
|
|
197
|
+
if (version !== undefined) {
|
|
198
|
+
const gemspecPath = path.join(specsDir, `${gemName}-${version}.gemspec`);
|
|
199
|
+
const content = yield* readFileOptional(gemspecPath);
|
|
200
|
+
if (Option.isNone(content))
|
|
201
|
+
return Option.none();
|
|
202
|
+
const axmMeta = parseAxmMetadataFromGemspec(content.value);
|
|
203
|
+
if (axmMeta === undefined)
|
|
204
|
+
return Option.none();
|
|
205
|
+
const metaResult = decodeAxmMeta(axmMeta);
|
|
206
|
+
if (Result.isFailure(metaResult)) {
|
|
207
|
+
yield* Effect.logWarning(`Invalid axm metadata in ${gemName}-${version}: schema validation failed`);
|
|
208
|
+
return Option.none();
|
|
209
|
+
}
|
|
210
|
+
return Option.some(metaResult.success.extensions);
|
|
211
|
+
}
|
|
212
|
+
// No version - scan specs directory for matching gemspec
|
|
213
|
+
const dirEntries = yield* fs.readDirectory(specsDir).pipe(Effect.option);
|
|
214
|
+
if (Option.isNone(dirEntries))
|
|
215
|
+
return Option.none();
|
|
216
|
+
const matchingSpec = dirEntries.value.find((entry) => entry.startsWith(`${gemName}-`) && entry.endsWith(".gemspec"));
|
|
217
|
+
if (matchingSpec === undefined)
|
|
218
|
+
return Option.none();
|
|
219
|
+
const gemspecPath = path.join(specsDir, matchingSpec);
|
|
220
|
+
const content = yield* readFileOptional(gemspecPath);
|
|
221
|
+
if (Option.isNone(content))
|
|
222
|
+
return Option.none();
|
|
223
|
+
const axmMeta = parseAxmMetadataFromGemspec(content.value);
|
|
224
|
+
if (axmMeta === undefined)
|
|
225
|
+
return Option.none();
|
|
226
|
+
const metaResult = decodeAxmMeta(axmMeta);
|
|
227
|
+
if (Result.isFailure(metaResult)) {
|
|
228
|
+
yield* Effect.logWarning(`Invalid axm metadata in ${gemName}: schema validation failed`);
|
|
229
|
+
return Option.none();
|
|
230
|
+
}
|
|
231
|
+
return Option.some(metaResult.success.extensions);
|
|
232
|
+
}, Effect.annotateLogs({ reader: "gem" }), Effect.withSpan("read.gem")),
|
|
233
|
+
};
|
|
234
|
+
//# sourceMappingURL=gem.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go module 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
|
+
* Go module package detector.
|
|
10
|
+
*
|
|
11
|
+
* Scans `go.mod` in the project directory and extracts direct dependencies
|
|
12
|
+
* from `require` directives, filtering out `// indirect` entries.
|
|
13
|
+
*
|
|
14
|
+
* @experimental This API is unstable and may change without notice.
|
|
15
|
+
*/
|
|
16
|
+
export declare const golangDetector: PackageDetector;
|
|
17
|
+
/**
|
|
18
|
+
* Go module package reader.
|
|
19
|
+
*
|
|
20
|
+
* Reads `axm.json` from `$GOPATH/pkg/mod/<module>@<version>/` for each
|
|
21
|
+
* detected Go module and extracts recommendation metadata.
|
|
22
|
+
*
|
|
23
|
+
* @experimental This API is unstable and may change without notice.
|
|
24
|
+
*/
|
|
25
|
+
export declare const golangReader: PackageReader;
|
|
26
|
+
//# sourceMappingURL=golang.d.ts.map
|