@agntn/registries 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/dist/_chunks/cached-registry.d.mts +35 -0
  4. package/dist/_chunks/cached-registry.d.mts.map +1 -0
  5. package/dist/_chunks/errors.d.mts +90 -0
  6. package/dist/_chunks/errors.d.mts.map +1 -0
  7. package/dist/_chunks/helpers.mjs +95 -0
  8. package/dist/_chunks/helpers.mjs.map +1 -0
  9. package/dist/_chunks/index.d.mts +142 -0
  10. package/dist/_chunks/index.d.mts.map +1 -0
  11. package/dist/_chunks/index2.d.mts +50 -0
  12. package/dist/_chunks/index2.d.mts.map +1 -0
  13. package/dist/_chunks/index3.d.mts +18 -0
  14. package/dist/_chunks/index3.d.mts.map +1 -0
  15. package/dist/_chunks/lockfile.d.mts +50 -0
  16. package/dist/_chunks/lockfile.d.mts.map +1 -0
  17. package/dist/_chunks/paths.d.mts +11 -0
  18. package/dist/_chunks/paths.d.mts.map +1 -0
  19. package/dist/_chunks/purl.mjs +192 -0
  20. package/dist/_chunks/purl.mjs.map +1 -0
  21. package/dist/_chunks/registries.mjs +1197 -0
  22. package/dist/_chunks/registries.mjs.map +1 -0
  23. package/dist/_chunks/registry.d.mts +39 -0
  24. package/dist/_chunks/registry.d.mts.map +1 -0
  25. package/dist/_chunks/registry.mjs +201 -0
  26. package/dist/_chunks/registry.mjs.map +1 -0
  27. package/dist/_chunks/storage.d.mts +22 -0
  28. package/dist/_chunks/storage.d.mts.map +1 -0
  29. package/dist/_chunks/types.d.mts +73 -0
  30. package/dist/_chunks/types.d.mts.map +1 -0
  31. package/dist/ai.d.mts +35 -0
  32. package/dist/ai.d.mts.map +1 -0
  33. package/dist/ai.mjs +50 -0
  34. package/dist/ai.mjs.map +1 -0
  35. package/dist/cache/cached-registry.d.mts +4 -0
  36. package/dist/cache/cached-registry.mjs +92 -0
  37. package/dist/cache/cached-registry.mjs.map +1 -0
  38. package/dist/cache/index.d.mts +8 -0
  39. package/dist/cache/index.mjs +12 -0
  40. package/dist/cache/index.mjs.map +1 -0
  41. package/dist/cache/lockfile.d.mts +2 -0
  42. package/dist/cache/lockfile.mjs +68 -0
  43. package/dist/cache/lockfile.mjs.map +1 -0
  44. package/dist/cache/paths.d.mts +2 -0
  45. package/dist/cache/paths.mjs +26 -0
  46. package/dist/cache/paths.mjs.map +1 -0
  47. package/dist/cache/storage.d.mts +2 -0
  48. package/dist/cache/storage.mjs +40 -0
  49. package/dist/cache/storage.mjs.map +1 -0
  50. package/dist/cli.d.mts +1 -0
  51. package/dist/cli.mjs +19 -0
  52. package/dist/cli.mjs.map +1 -0
  53. package/dist/commands/cache.d.mts +7 -0
  54. package/dist/commands/cache.d.mts.map +1 -0
  55. package/dist/commands/cache.mjs +80 -0
  56. package/dist/commands/cache.mjs.map +1 -0
  57. package/dist/commands/deps.d.mts +23 -0
  58. package/dist/commands/deps.d.mts.map +1 -0
  59. package/dist/commands/deps.mjs +72 -0
  60. package/dist/commands/deps.mjs.map +1 -0
  61. package/dist/commands/info.d.mts +26 -0
  62. package/dist/commands/info.d.mts.map +1 -0
  63. package/dist/commands/info.mjs +54 -0
  64. package/dist/commands/info.mjs.map +1 -0
  65. package/dist/commands/maintainers.d.mts +23 -0
  66. package/dist/commands/maintainers.d.mts.map +1 -0
  67. package/dist/commands/maintainers.mjs +46 -0
  68. package/dist/commands/maintainers.mjs.map +1 -0
  69. package/dist/commands/shared.d.mts +27 -0
  70. package/dist/commands/shared.d.mts.map +1 -0
  71. package/dist/commands/shared.mjs +69 -0
  72. package/dist/commands/shared.mjs.map +1 -0
  73. package/dist/commands/versions.d.mts +31 -0
  74. package/dist/commands/versions.d.mts.map +1 -0
  75. package/dist/commands/versions.mjs +58 -0
  76. package/dist/commands/versions.mjs.map +1 -0
  77. package/dist/core/index.d.mts +4 -0
  78. package/dist/core/index.mjs +3 -0
  79. package/dist/index.d.mts +11 -0
  80. package/dist/index.mjs +10 -0
  81. package/dist/registries/index.d.mts +4 -0
  82. package/dist/registries/index.mjs +4 -0
  83. package/dist/types.d.mts +3 -0
  84. package/dist/types.mjs +1 -0
  85. package/package.json +102 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present oritwoen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # @agntn/registries
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40agntn%2Fregistries?style=flat&colorA=130f40&colorB=474787)](https://npmjs.com/package/@agntn/registries)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40agntn%2Fregistries?style=flat&colorA=130f40&colorB=474787)](https://npm.chart.dev/@agntn/registries)
5
+ [![license](https://img.shields.io/github/license/agntn/registries?style=flat&colorA=130f40&colorB=474787)](https://github.com/agntn/registries/blob/main/LICENSE)
6
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/agntn/registries)
7
+
8
+ > Query npm, PyPI, crates.io, RubyGems, Packagist, and Arch Linux with one API. PURL-native, typed, cached.
9
+
10
+ ## Why?
11
+
12
+ If you need package metadata from multiple registries, you currently have two options: call each registry's REST API yourself (they all work differently), or depend on a third-party aggregation service.
13
+
14
+ There is no embeddable TypeScript library that normalizes across registries. The closest thing is [git-pkgs/registries](https://github.com/git-pkgs/registries) in Go, which covers 25 ecosystems but is Go-only. Aggregation APIs like [ecosyste.ms](https://ecosyste.ms/) and [deps.dev](https://deps.dev/) exist, but they are external services you can't bundle into your own tool.
15
+
16
+ @agntn/registries fills that gap. One `fetchPackage` call, same response shape, regardless of whether the package lives on npm or Packagist. Uses [PURL (ECMA-427)](https://github.com/package-url/purl-spec) for addressing, so `pkg:npm/lodash` and `pkg:cargo/serde` resolve through the same code path. Storage-backed caching with a lockfile keeps things fast on repeated lookups.
17
+
18
+ ## Features
19
+
20
+ - 🔍 **Single API, six registries** - npm, PyPI, crates.io, RubyGems, Packagist, Arch Linux (official + AUR)
21
+ - 📦 **PURL-native** - [ECMA-427](https://github.com/package-url/purl-spec) identifiers as first-class input
22
+ - 🏷️ **Normalized data model** - same `Package`, `Version`, `Dependency`, `Maintainer` types everywhere
23
+ - 💾 **Storage-backed cache + lockfile** - unstorage-native, sha256 integrity checks, configurable TTL
24
+ - ⌨️ **CLI included** - `registries info npm/lodash`, `registries versions cargo/serde`, `registries deps pypi/flask@3.1.1`
25
+ - 🔁 **Retry + backoff** - exponential backoff with jitter, rate limiter interface
26
+ - 🪶 **ESM-only, zero CJS** - built with [obuild](https://github.com/unjs/obuild)
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pnpm add @agntn/registries
32
+ ```
33
+
34
+ For the AI SDK tool (`@agntn/registries/ai` subpath), also install `ai` and `zod`:
35
+
36
+ ```bash
37
+ pnpm add ai zod
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ### API
43
+
44
+ ```ts
45
+ import { fetchPackageFromPURL } from "@agntn/registries";
46
+
47
+ const pkg = await fetchPackageFromPURL("pkg:npm/lodash");
48
+
49
+ console.log(pkg.name); // "lodash"
50
+ console.log(pkg.latestVersion); // "4.17.23"
51
+ console.log(pkg.licenses); // "MIT"
52
+ console.log(pkg.repository); // "https://github.com/lodash/lodash"
53
+ ```
54
+
55
+ Works the same for any supported registry:
56
+
57
+ ```ts
58
+ await fetchPackageFromPURL("pkg:cargo/serde");
59
+ await fetchPackageFromPURL("pkg:pypi/flask");
60
+ await fetchPackageFromPURL("pkg:gem/rails");
61
+ await fetchPackageFromPURL("pkg:composer/laravel/framework");
62
+ await fetchPackageFromPURL("pkg:alpm/arch/pacman");
63
+ ```
64
+
65
+ ### CLI
66
+
67
+ The `pkg:` prefix is optional in the CLI. `npm/lodash` works just as well:
68
+
69
+ ```bash
70
+ registries info npm/lodash
71
+ registries versions cargo/serde
72
+ registries deps pypi/flask@3.1.1
73
+ registries maintainers gem/rails
74
+ registries deps alpm/aur/paru
75
+ ```
76
+
77
+ Add `--json` for machine-readable output, `--no-cache` to skip the cache.
78
+
79
+ ### AI SDK tool
80
+
81
+ `@agntn/registries/ai` exports a ready-made tool for AI SDK apps:
82
+
83
+ ```ts
84
+ import { generateText } from "ai";
85
+ import { packageTool } from "@agntn/registries/ai";
86
+
87
+ const { text } = await generateText({
88
+ model: yourModel,
89
+ tools: { packageRegistry: packageTool },
90
+ prompt: "Show me the latest metadata for pkg:npm/lodash and then list its maintainers.",
91
+ });
92
+ ```
93
+
94
+ The tool supports these operations through one input schema:
95
+
96
+ ```ts
97
+ // { operation: 'package', purl: 'pkg:npm/lodash' }
98
+ // { operation: 'versions', purl: 'pkg:cargo/serde' }
99
+ // { operation: 'dependencies', purl: 'pkg:pypi/flask@3.1.1' }
100
+ // { operation: 'maintainers', purl: 'pkg:gem/rails' }
101
+ // { operation: 'bulk-packages', purls: ['pkg:npm/lodash', 'pkg:cargo/serde'], concurrency?: number }
102
+ ```
103
+
104
+ ## Registries
105
+
106
+ | Ecosystem | PURL type | Registry |
107
+ | ---------- | ------------------ | -------------------------------- |
108
+ | npm | `pkg:npm/...` | registry.npmjs.org |
109
+ | Cargo | `pkg:cargo/...` | crates.io |
110
+ | PyPI | `pkg:pypi/...` | pypi.org |
111
+ | RubyGems | `pkg:gem/...` | rubygems.org |
112
+ | Packagist | `pkg:composer/...` | packagist.org |
113
+ | Arch Linux | `pkg:alpm/...` | archlinux.org, aur.archlinux.org |
114
+
115
+ Scoped packages work as expected: `pkg:npm/%40vue/core` or `npm/@vue/core` in the CLI.
116
+
117
+ Arch Linux packages use a namespace: `pkg:alpm/arch/pacman` (or just `pkg:alpm/pacman`) for official repos, `pkg:alpm/aur/paru` for AUR. Official packages default to `arch` when the namespace is omitted; AUR requires the explicit `aur` namespace.
118
+
119
+ ## API reference
120
+
121
+ ### PURL helpers
122
+
123
+ ```ts
124
+ import {
125
+ fetchPackageFromPURL,
126
+ fetchVersionsFromPURL,
127
+ fetchDependenciesFromPURL,
128
+ fetchMaintainersFromPURL,
129
+ bulkFetchPackages,
130
+ } from "@agntn/registries";
131
+
132
+ // Single lookups
133
+ const pkg = await fetchPackageFromPURL("pkg:npm/lodash");
134
+ const versions = await fetchVersionsFromPURL("pkg:cargo/serde");
135
+ const deps = await fetchDependenciesFromPURL("pkg:pypi/flask@3.1.1");
136
+ const maintainers = await fetchMaintainersFromPURL("pkg:gem/rails");
137
+
138
+ // Bulk - fetches up to 15 packages concurrently
139
+ const packages = await bulkFetchPackages(["pkg:npm/lodash", "pkg:cargo/serde", "pkg:pypi/flask"]);
140
+ ```
141
+
142
+ ### Direct registry access
143
+
144
+ For more control, instantiate a concrete registry class:
145
+
146
+ ```ts
147
+ import { Client } from "@agntn/registries";
148
+ import { NpmRegistry } from "@agntn/registries";
149
+
150
+ const npm = new NpmRegistry("https://registry.npmjs.org", new Client());
151
+ const pkg = await npm.fetchPackage("lodash");
152
+ const versions = await npm.fetchVersions("lodash");
153
+ const deps = await npm.fetchDependencies("lodash", "4.17.21");
154
+ ```
155
+
156
+ Use `create("npm")` when you prefer lookup through the registered ecosystem classes.
157
+
158
+ ### Cached registry
159
+
160
+ Wrap any registry with caching:
161
+
162
+ ```ts
163
+ import { createCached } from "@agntn/registries";
164
+
165
+ const npm = createCached("npm");
166
+
167
+ // First call hits the network and writes to cache
168
+ const pkg = await npm.fetchPackage("lodash");
169
+
170
+ // Second call reads from cache (if TTL hasn't expired)
171
+ const same = await npm.fetchPackage("lodash");
172
+ ```
173
+
174
+ By default, @agntn/registries uses filesystem storage and follows platform cache conventions: `~/.cache/registries` on Linux (XDG), `~/Library/Caches/registries` on macOS, `%LOCALAPPDATA%\registries\cache` on Windows. Override with `REGISTRIES_CACHE_DIR` env var.
175
+
176
+ For edge/serverless runtimes, configure a custom unstorage driver (example: Cloudflare KV binding):
177
+
178
+ ```ts
179
+ import { configureStorage, createCached } from "@agntn/registries";
180
+ import { createStorage } from "unstorage";
181
+ import cloudflareKVBindingDriver from "unstorage/drivers/cloudflare-kv-binding";
182
+
183
+ configureStorage(
184
+ createStorage({
185
+ driver: cloudflareKVBindingDriver({ binding: "REGISTRIES_CACHE" }),
186
+ }),
187
+ );
188
+
189
+ const npm = createCached("npm");
190
+ const pkg = await npm.fetchPackage("lodash");
191
+ ```
192
+
193
+ ### PURL parsing
194
+
195
+ ```ts
196
+ import { parsePURL, buildPURL, fullName } from "@agntn/registries";
197
+
198
+ const parsed = parsePURL("pkg:npm/%40vue/core@3.5.0");
199
+ // { type: 'npm', namespace: '@vue', name: 'core', version: '3.5.0', qualifiers: {}, subpath: '' }
200
+
201
+ fullName(parsed); // "@vue/core"
202
+
203
+ buildPURL({ type: "cargo", name: "serde", version: "1.0.0" });
204
+ // "pkg:cargo/serde@1.0.0"
205
+ ```
206
+
207
+ ### Types
208
+
209
+ ```ts
210
+ import type {
211
+ Package,
212
+ Version,
213
+ Dependency,
214
+ Maintainer,
215
+ Registry,
216
+ ParsedPURL,
217
+ } from "@agntn/registries";
218
+ ```
219
+
220
+ ## CLI
221
+
222
+ ```bash
223
+ registries <command> [options]
224
+ ```
225
+
226
+ | Command | Description |
227
+ | ------------------------------- | ------------------------------------------------------ |
228
+ | `registries info <purl>` | Package metadata (name, license, repo, latest version) |
229
+ | `registries versions <purl>` | List all published versions |
230
+ | `registries deps <purl>` | Dependencies for a specific version |
231
+ | `registries maintainers <purl>` | Package maintainers / authors |
232
+ | `registries cache status` | Show cache stats (entries, freshness) |
233
+ | `registries cache path` | Print cache directory path |
234
+ | `registries cache clear` | Remove all cached data |
235
+ | `registries cache prune` | Remove stale entries |
236
+
237
+ ### Options
238
+
239
+ | Flag | Description |
240
+ | ------------ | ---------------------------------------- |
241
+ | `--json` | Output as JSON |
242
+ | `--no-cache` | Bypass cache, always fetch from registry |
243
+
244
+ ## Caching
245
+
246
+ @agntn/registries stores fetched data and freshness metadata in unstorage. Default TTLs:
247
+
248
+ | Data type | TTL |
249
+ | ---------------- | ---------- |
250
+ | Package metadata | 1 hour |
251
+ | Version list | 30 minutes |
252
+ | Dependencies | 24 hours |
253
+ | Maintainers | 24 hours |
254
+
255
+ Each cached entry has a sha256 integrity hash. If the stored data doesn't match the hash, @agntn/registries refetches automatically.
256
+
257
+ ## Data model
258
+
259
+ Every registry returns the same normalized types:
260
+
261
+ ```ts
262
+ interface Package {
263
+ name: string;
264
+ description: string;
265
+ homepage: string;
266
+ documentation: string; // docs URL (docs.rs, readthedocs, rubydoc, etc.)
267
+ repository: string;
268
+ licenses: string; // SPDX-normalized
269
+ keywords: string[];
270
+ namespace: string; // e.g. "@vue" for npm scoped packages
271
+ latestVersion: string;
272
+ metadata: Record<string, unknown>;
273
+ }
274
+
275
+ interface Version {
276
+ number: string;
277
+ publishedAt: Date | null;
278
+ licenses: string;
279
+ integrity: string;
280
+ status: "" | "yanked" | "deprecated" | "retracted";
281
+ metadata: Record<string, unknown>;
282
+ }
283
+
284
+ interface Dependency {
285
+ name: string;
286
+ requirements: string; // version constraint
287
+ scope: "runtime" | "development" | "test" | "build" | "optional";
288
+ optional: boolean;
289
+ }
290
+
291
+ interface Maintainer {
292
+ uuid: string;
293
+ login: string;
294
+ name: string;
295
+ email: string;
296
+ url: string;
297
+ role: string;
298
+ }
299
+ ```
300
+
301
+ ## License
302
+
303
+ [MIT](./LICENSE)
@@ -0,0 +1,35 @@
1
+ import { c as URLBuilder, i as Package, l as Version, n as Dependency, r as Maintainer } from "./types.mjs";
2
+ import { t as Registry } from "./registry.mjs";
3
+ import { Storage } from "unstorage";
4
+
5
+ //#region src/cache/cached-registry.d.ts
6
+ /**
7
+ * CachedRegistry wraps any Registry and adds caching backed by unstorage
8
+ * with a lockfile for freshness tracking.
9
+ *
10
+ * Pass a custom `storage` to use a non-default driver (e.g. Cloudflare KV).
11
+ * If omitted, uses the globally configured storage (see `configureStorage()`).
12
+ */
13
+ declare class CachedRegistry extends Registry {
14
+ readonly inner: Registry;
15
+ readonly storage: Storage;
16
+ private readonly lockfileStorage;
17
+ private readonly inflight;
18
+ constructor(inner: Registry, storage?: Storage);
19
+ ecosystem(): string;
20
+ urls(): URLBuilder;
21
+ fetchPackage(name: string, signal?: AbortSignal): Promise<Package>;
22
+ fetchVersions(name: string, signal?: AbortSignal): Promise<Version[]>;
23
+ fetchDependencies(name: string, version: string, signal?: AbortSignal): Promise<Dependency[]>;
24
+ fetchMaintainers(name: string, signal?: AbortSignal): Promise<Maintainer[]>;
25
+ /**
26
+ * Generic cache-through method:
27
+ * 1. Check lockfile for fresh entry
28
+ * 2. If fresh -> read from storage
29
+ * 3. If stale/missing -> fetch, store, update lockfile
30
+ */
31
+ private cached;
32
+ }
33
+ //#endregion
34
+ export { CachedRegistry as t };
35
+ //# sourceMappingURL=cached-registry.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cached-registry.d.mts","names":[],"sources":["../../src/cache/cached-registry.ts"],"mappings":";;;;;;;AAqBA;;;;;cAAa,cAAA,SAAuB,QAAA;EAAA,SACzB,KAAA,EAAO,QAAA;EAAA,SACP,OAAA,EAAS,OAAA;EAAA,iBACD,eAAA;EAAA,iBACA,QAAA;cAEL,KAAA,EAAO,QAAA,EAAU,OAAA,GAAU,OAAA;EAOvC,SAAA,CAAA;EAIA,IAAA,CAAA,GAAQ,UAAA;EAIF,YAAA,CAAa,IAAA,UAAc,MAAA,GAAS,WAAA,GAAc,OAAA,CAAQ,OAAA;EAW1D,aAAA,CAAc,IAAA,UAAc,MAAA,GAAS,WAAA,GAAc,OAAA,CAAQ,OAAA;EAgB3D,iBAAA,CACJ,IAAA,UACA,OAAA,UACA,MAAA,GAAS,WAAA,GACR,OAAA,CAAQ,UAAA;EAUL,gBAAA,CAAiB,IAAA,UAAc,MAAA,GAAS,WAAA,GAAc,OAAA,CAAQ,UAAA;EAAtB;;;;;;EAAA,QAgBhC,MAAA;AAAA"}
@@ -0,0 +1,90 @@
1
+ import { a as ParsedPURL } from "./types.mjs";
2
+ import { o as Client, t as Registry } from "./registry.mjs";
3
+
4
+ //#region src/core/purl.d.ts
5
+ /**
6
+ * Parse a PURL string into its components.
7
+ *
8
+ * Format: `pkg:<type>/<namespace>/<name>@<version>?<qualifiers>#<subpath>`
9
+ *
10
+ * @see https://github.com/package-url/purl-spec (ECMA-427)
11
+ */
12
+ declare function parsePURL(purlStr: string): ParsedPURL;
13
+ /** Build the full name from namespace + name (e.g., "@scope/pkg" for npm). */
14
+ declare function fullName(parsed: ParsedPURL): string;
15
+ /** Create a registry instance from a PURL, returning [registry, name, version]. */
16
+ declare function createFromPURL(purlStr: string, client?: Client): [Registry, string, string];
17
+ /** Build a PURL string from components. Inverse of `parsePURL`. */
18
+ declare function buildPURL(parts: {
19
+ type: string;
20
+ name: string;
21
+ version?: string;
22
+ namespace?: string;
23
+ qualifiers?: Record<string, string>;
24
+ subpath?: string;
25
+ }): string;
26
+ //#endregion
27
+ //#region src/core/license.d.ts
28
+ /**
29
+ * Normalize a license string to SPDX expression.
30
+ *
31
+ * Handles common variations: "MIT License" → "MIT", "Apache 2.0" → "Apache-2.0".
32
+ * Passes through already-valid SPDX identifiers unchanged.
33
+ */
34
+ declare function normalizeLicense(raw: string | null | undefined): string;
35
+ /**
36
+ * Combine multiple license strings into a single SPDX expression.
37
+ *
38
+ * Default operator is OR (disjunctive, user picks one).
39
+ * Pass "AND" for ecosystems where multiple licenses mean all apply (e.g. Arch Linux).
40
+ */
41
+ declare function combineLicenses(licenses: (string | null | undefined)[], operator?: "OR" | "AND"): string;
42
+ //#endregion
43
+ //#region src/core/repository.d.ts
44
+ /**
45
+ * Normalize a repository URL to a clean HTTPS git URL.
46
+ *
47
+ * Handles various formats:
48
+ * - "git+https://github.com/foo/bar.git" → "https://github.com/foo/bar"
49
+ * - "git://github.com/foo/bar" → "https://github.com/foo/bar"
50
+ * - "ssh://git@github.com/foo/bar.git" → "https://github.com/foo/bar"
51
+ * - "github:foo/bar" → "https://github.com/foo/bar"
52
+ * - { url: "..." } → extracted and normalized
53
+ */
54
+ declare function normalizeRepositoryURL(raw: unknown): string;
55
+ //#endregion
56
+ //#region src/core/errors.d.ts
57
+ /** Custom error types for registry operations. */
58
+ declare class PkioError extends Error {
59
+ constructor(message: string, options?: ErrorOptions);
60
+ }
61
+ declare class HTTPError extends PkioError {
62
+ readonly statusCode: number;
63
+ readonly url: string;
64
+ readonly body: string;
65
+ constructor(statusCode: number, url: string, body: string);
66
+ isNotFound(): boolean;
67
+ isRateLimit(): boolean;
68
+ isServerError(): boolean;
69
+ }
70
+ declare class NotFoundError extends PkioError {
71
+ readonly ecosystem: string;
72
+ readonly packageName: string;
73
+ readonly version: string;
74
+ constructor(ecosystem: string, packageName: string, version?: string);
75
+ }
76
+ declare class RateLimitError extends PkioError {
77
+ readonly retryAfter: number;
78
+ constructor(retryAfter: number);
79
+ }
80
+ declare class UnknownEcosystemError extends PkioError {
81
+ readonly ecosystem: string;
82
+ constructor(ecosystem: string);
83
+ }
84
+ declare class InvalidPURLError extends PkioError {
85
+ readonly purl: string;
86
+ constructor(purl: string, reason: string);
87
+ }
88
+ //#endregion
89
+ export { RateLimitError as a, combineLicenses as c, createFromPURL as d, fullName as f, PkioError as i, normalizeLicense as l, InvalidPURLError as n, UnknownEcosystemError as o, parsePURL as p, NotFoundError as r, normalizeRepositoryURL as s, HTTPError as t, buildPURL as u };
90
+ //# sourceMappingURL=errors.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/core/purl.ts","../../src/core/license.ts","../../src/core/repository.ts","../../src/core/errors.ts"],"mappings":";;;;;AAqBA;;;;;AA4FA;iBA5FgB,SAAA,CAAU,OAAA,WAAkB,UAAA;;iBA4F5B,QAAA,CAAS,MAAA,EAAQ,UAAA;;iBAQjB,cAAA,CAAe,OAAA,UAAiB,MAAA,GAAS,MAAA,IAAU,QAAA;;iBAQnD,SAAA,CAAU,KAAA;EACxB,IAAA;EACA,IAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA,GAAa,MAAA;EACb,OAAA;AAAA;;;;;;;AAlHF;;iBCfgB,gBAAA,CAAiB,GAAA;;;AD2GjC;;;;iBClDgB,eAAA,CACd,QAAA,iCACA,QAAA;;;;;;;AD5CF;;;;;AA4FA;iBEvGgB,sBAAA,CAAuB,GAAA;;;;cCR1B,SAAA,SAAkB,KAAA;cACjB,OAAA,UAAiB,OAAA,GAAU,YAAA;AAAA;AAAA,cAM5B,SAAA,SAAkB,SAAA;EAAA,SACpB,UAAA;EAAA,SACA,GAAA;EAAA,SACA,IAAA;cAEG,UAAA,UAAoB,GAAA,UAAa,IAAA;EAQ7C,UAAA,CAAA;EAIA,WAAA,CAAA;EAIA,aAAA,CAAA;AAAA;AAAA,cAKW,aAAA,SAAsB,SAAA;EAAA,SACxB,SAAA;EAAA,SACA,WAAA;EAAA,SACA,OAAA;cAEG,SAAA,UAAmB,WAAA,UAAqB,OAAA;AAAA;AAAA,cAUzC,cAAA,SAAuB,SAAA;EAAA,SACzB,UAAA;cAEG,UAAA;AAAA;AAAA,cAOD,qBAAA,SAA8B,SAAA;EAAA,SAChC,SAAA;cAEG,SAAA;AAAA;AAAA,cAOD,gBAAA,SAAyB,SAAA;EAAA,SAC3B,IAAA;cAEG,IAAA,UAAc,MAAA;AAAA"}
@@ -0,0 +1,95 @@
1
+ import { l as InvalidPURLError } from "./registry.mjs";
2
+ import { n as createFromPURL } from "./purl.mjs";
3
+ /** Fetch normalized package metadata from a PURL. */
4
+ async function fetchPackageFromPURL(purl, signal, client) {
5
+ const [reg, name] = createFromPURL(purl, client);
6
+ return reg.fetchPackage(name, signal);
7
+ }
8
+ /** Fetch all versions from a PURL. */
9
+ async function fetchVersionsFromPURL(purl, signal, client) {
10
+ const [reg, name] = createFromPURL(purl, client);
11
+ return reg.fetchVersions(name, signal);
12
+ }
13
+ /** Fetch dependencies for a specific version from a PURL. */
14
+ async function fetchDependenciesFromPURL(purl, signal, client) {
15
+ const [reg, name, version] = createFromPURL(purl, client);
16
+ if (!version) throw new InvalidPURLError(purl, "must include a version for dependency lookup");
17
+ return reg.fetchDependencies(name, version, signal);
18
+ }
19
+ /** Fetch maintainers from a PURL. */
20
+ async function fetchMaintainersFromPURL(purl, signal, client) {
21
+ const [reg, name] = createFromPURL(purl, client);
22
+ return reg.fetchMaintainers(name, signal);
23
+ }
24
+ const DEFAULT_CONCURRENCY = 15;
25
+ /** Bulk fetch packages from multiple PURLs, with concurrency limit. */
26
+ async function bulkFetchPackages(purls, options) {
27
+ const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;
28
+ const results = /* @__PURE__ */ new Map();
29
+ const queue = [...purls];
30
+ const signal = options?.signal;
31
+ const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
32
+ while (queue.length > 0) {
33
+ if (signal?.aborted) break;
34
+ const purl = queue.shift();
35
+ try {
36
+ const pkg = await fetchPackageFromPURL(purl, signal, options?.client);
37
+ results.set(purl, pkg);
38
+ } catch {
39
+ if (signal?.aborted) break;
40
+ }
41
+ }
42
+ });
43
+ await Promise.all(workers);
44
+ return results;
45
+ }
46
+ /**
47
+ * Select the best matching version from a list.
48
+ *
49
+ * Resolution order:
50
+ * 1. Exact match for `requested` (non-yanked/deprecated/retracted)
51
+ * 2. Exact match for `latest` (non-yanked/deprecated/retracted)
52
+ * 3. Newest available version with no negative status (by publishedAt)
53
+ *
54
+ * Returns `null` when no usable version exists.
55
+ */
56
+ function selectVersion(versions, options) {
57
+ const { requested, latest } = options ?? {};
58
+ if (requested) {
59
+ const exact = versions.find((v) => v.number === requested && v.status === "");
60
+ if (exact) return exact;
61
+ }
62
+ if (latest) {
63
+ const latestV = versions.find((v) => v.number === latest && v.status === "");
64
+ if (latestV) return latestV;
65
+ }
66
+ const usable = versions.filter((v) => v.status === "");
67
+ if (usable.length === 0) return null;
68
+ usable.sort((a, b) => {
69
+ const at = a.publishedAt?.getTime() ?? 0;
70
+ return (b.publishedAt?.getTime() ?? 0) - at;
71
+ });
72
+ return usable[0] ?? null;
73
+ }
74
+ /**
75
+ * Resolve the best documentation URL for a package.
76
+ *
77
+ * Fallback chain:
78
+ * 1. `package.documentation` (explicit docs URL from registry)
79
+ * 2. `package.homepage` (project homepage)
80
+ * 3. `urls.documentation()` (ecosystem default, e.g. docs.rs, rubydoc.info)
81
+ */
82
+ function resolveDocsUrl(pkg, urls, version) {
83
+ return pkg.documentation || pkg.homepage || urls.documentation(pkg.name, version);
84
+ }
85
+ /**
86
+ * Resolve the best README URL for a package.
87
+ *
88
+ * Returns the ecosystem-specific URL where the package README can be fetched.
89
+ */
90
+ function resolveReadmeUrl(pkg, urls, version) {
91
+ return urls.readme(pkg.name, version);
92
+ }
93
+ export { fetchVersionsFromPURL as a, selectVersion as c, fetchPackageFromPURL as i, fetchDependenciesFromPURL as n, resolveDocsUrl as o, fetchMaintainersFromPURL as r, resolveReadmeUrl as s, bulkFetchPackages as t };
94
+
95
+ //# sourceMappingURL=helpers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/helpers.ts"],"sourcesContent":["import type { Package, Version, Dependency, Maintainer, URLBuilder } from \"./core/types.ts\";\nimport type { Client } from \"./core/client.ts\";\nimport { createFromPURL } from \"./core/purl.ts\";\nimport { InvalidPURLError } from \"./core/errors.ts\";\n\n/** Fetch normalized package metadata from a PURL. */\nexport async function fetchPackageFromPURL(\n purl: string,\n signal?: AbortSignal,\n client?: Client,\n): Promise<Package> {\n const [reg, name] = createFromPURL(purl, client);\n return reg.fetchPackage(name, signal);\n}\n\n/** Fetch all versions from a PURL. */\nexport async function fetchVersionsFromPURL(\n purl: string,\n signal?: AbortSignal,\n client?: Client,\n): Promise<Version[]> {\n const [reg, name] = createFromPURL(purl, client);\n return reg.fetchVersions(name, signal);\n}\n\n/** Fetch dependencies for a specific version from a PURL. */\nexport async function fetchDependenciesFromPURL(\n purl: string,\n signal?: AbortSignal,\n client?: Client,\n): Promise<Dependency[]> {\n const [reg, name, version] = createFromPURL(purl, client);\n if (!version) {\n throw new InvalidPURLError(purl, \"must include a version for dependency lookup\");\n }\n return reg.fetchDependencies(name, version, signal);\n}\n\n/** Fetch maintainers from a PURL. */\nexport async function fetchMaintainersFromPURL(\n purl: string,\n signal?: AbortSignal,\n client?: Client,\n): Promise<Maintainer[]> {\n const [reg, name] = createFromPURL(purl, client);\n return reg.fetchMaintainers(name, signal);\n}\n\nconst DEFAULT_CONCURRENCY = 15;\n\n/** Bulk fetch packages from multiple PURLs, with concurrency limit. */\nexport async function bulkFetchPackages(\n purls: string[],\n options?: { concurrency?: number; signal?: AbortSignal; client?: Client },\n): Promise<Map<string, Package>> {\n const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;\n const results = new Map<string, Package>();\n const queue = [...purls];\n\n const signal = options?.signal;\n\n const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {\n while (queue.length > 0) {\n if (signal?.aborted) break;\n\n const purl = queue.shift()!;\n try {\n const pkg = await fetchPackageFromPURL(purl, signal, options?.client);\n results.set(purl, pkg);\n } catch {\n if (signal?.aborted) break;\n // Silently skip failed lookups — absent from results map\n }\n }\n });\n\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Select the best matching version from a list.\n *\n * Resolution order:\n * 1. Exact match for `requested` (non-yanked/deprecated/retracted)\n * 2. Exact match for `latest` (non-yanked/deprecated/retracted)\n * 3. Newest available version with no negative status (by publishedAt)\n *\n * Returns `null` when no usable version exists.\n */\nexport function selectVersion(\n versions: Version[],\n options?: {\n requested?: string;\n latest?: string;\n },\n): Version | null {\n const { requested, latest } = options ?? {};\n\n if (requested) {\n const exact = versions.find((v) => v.number === requested && v.status === \"\");\n if (exact) return exact;\n }\n\n if (latest) {\n const latestV = versions.find((v) => v.number === latest && v.status === \"\");\n if (latestV) return latestV;\n }\n\n const usable = versions.filter((v) => v.status === \"\");\n if (usable.length === 0) return null;\n\n usable.sort((a, b) => {\n const at = a.publishedAt?.getTime() ?? 0;\n const bt = b.publishedAt?.getTime() ?? 0;\n return bt - at;\n });\n\n return usable[0] ?? null;\n}\n\n/**\n * Resolve the best documentation URL for a package.\n *\n * Fallback chain:\n * 1. `package.documentation` (explicit docs URL from registry)\n * 2. `package.homepage` (project homepage)\n * 3. `urls.documentation()` (ecosystem default, e.g. docs.rs, rubydoc.info)\n */\nexport function resolveDocsUrl(pkg: Package, urls: URLBuilder, version?: string): string {\n return pkg.documentation || pkg.homepage || urls.documentation(pkg.name, version);\n}\n\n/**\n * Resolve the best README URL for a package.\n *\n * Returns the ecosystem-specific URL where the package README can be fetched.\n */\nexport function resolveReadmeUrl(pkg: Package, urls: URLBuilder, version?: string): string {\n return urls.readme(pkg.name, version);\n}\n"],"mappings":";;;AAMA,eAAsB,qBACpB,MACA,QACA,QACkB;CAClB,MAAM,CAAC,KAAK,QAAQ,eAAe,MAAM,OAAO;AAChD,QAAO,IAAI,aAAa,MAAM,OAAO;;;AAIvC,eAAsB,sBACpB,MACA,QACA,QACoB;CACpB,MAAM,CAAC,KAAK,QAAQ,eAAe,MAAM,OAAO;AAChD,QAAO,IAAI,cAAc,MAAM,OAAO;;;AAIxC,eAAsB,0BACpB,MACA,QACA,QACuB;CACvB,MAAM,CAAC,KAAK,MAAM,WAAW,eAAe,MAAM,OAAO;AACzD,KAAI,CAAC,QACH,OAAM,IAAI,iBAAiB,MAAM,+CAA+C;AAElF,QAAO,IAAI,kBAAkB,MAAM,SAAS,OAAO;;;AAIrD,eAAsB,yBACpB,MACA,QACA,QACuB;CACvB,MAAM,CAAC,KAAK,QAAQ,eAAe,MAAM,OAAO;AAChD,QAAO,IAAI,iBAAiB,MAAM,OAAO;;AAG3C,MAAM,sBAAsB;;AAG5B,eAAsB,kBACpB,OACA,SAC+B;CAC/B,MAAM,cAAc,SAAS,eAAe;CAC5C,MAAM,0BAAU,IAAI,KAAsB;CAC1C,MAAM,QAAQ,CAAC,GAAG,MAAM;CAExB,MAAM,SAAS,SAAS;CAExB,MAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,MAAM,OAAO,EAAE,EAAE,YAAY;AACtF,SAAO,MAAM,SAAS,GAAG;AACvB,OAAI,QAAQ,QAAS;GAErB,MAAM,OAAO,MAAM,OAAO;AAC1B,OAAI;IACF,MAAM,MAAM,MAAM,qBAAqB,MAAM,QAAQ,SAAS,OAAO;AACrE,YAAQ,IAAI,MAAM,IAAI;WAChB;AACN,QAAI,QAAQ,QAAS;;;GAIzB;AAEF,OAAM,QAAQ,IAAI,QAAQ;AAC1B,QAAO;;;;;;;;;;;;AAaT,SAAgB,cACd,UACA,SAIgB;CAChB,MAAM,EAAE,WAAW,WAAW,WAAW,EAAE;AAE3C,KAAI,WAAW;EACb,MAAM,QAAQ,SAAS,MAAM,MAAM,EAAE,WAAW,aAAa,EAAE,WAAW,GAAG;AAC7E,MAAI,MAAO,QAAO;;AAGpB,KAAI,QAAQ;EACV,MAAM,UAAU,SAAS,MAAM,MAAM,EAAE,WAAW,UAAU,EAAE,WAAW,GAAG;AAC5E,MAAI,QAAS,QAAO;;CAGtB,MAAM,SAAS,SAAS,QAAQ,MAAM,EAAE,WAAW,GAAG;AACtD,KAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,QAAO,MAAM,GAAG,MAAM;EACpB,MAAM,KAAK,EAAE,aAAa,SAAS,IAAI;AAEvC,UADW,EAAE,aAAa,SAAS,IAAI,KAC3B;GACZ;AAEF,QAAO,OAAO,MAAM;;;;;;;;;;AAWtB,SAAgB,eAAe,KAAc,MAAkB,SAA0B;AACvF,QAAO,IAAI,iBAAiB,IAAI,YAAY,KAAK,cAAc,IAAI,MAAM,QAAQ;;;;;;;AAQnF,SAAgB,iBAAiB,KAAc,MAAkB,SAA0B;AACzF,QAAO,KAAK,OAAO,IAAI,MAAM,QAAQ"}