diff --git a/docs/implementation/cli-commands.md b/docs/implementation/cli-commands.md index de6ec913..e37c92f8 100644 --- a/docs/implementation/cli-commands.md +++ b/docs/implementation/cli-commands.md @@ -16,7 +16,7 @@ The CLI exposes `example`, `languages`, `feedback`, top-level indexed `search` / | `feedback ` | `--accept` or `--reject` | `-m, --message `, `--json` | Submit feedback on a search result | | `pkg info ` | package spec | `--verbose`, `--json` | Show a package overview (latest version, downloads, license, vulnerabilities) | | `pkg vulns ` | package spec (optional `@version`) | `--severity`, `--include-withdrawn`, `--verbose`, `--json` | List known vulnerabilities for a package (npm/pypi/hex/crates) | -| `pkg deps ` | package spec (optional `@version`) | `--groups`, `--lifecycle`, `--transitive`, `--depth`, `--verbose`, `--json` | Analyse dependencies: direct runtime deps, structured groups, optional transitive graph (npm/pypi/hex/crates/vcpkg/zig) | +| `pkg deps ` | package spec (optional `@version`) | `--groups`, `--lifecycle`, `--transitive`, `--depth`, `--verbose`, `--json` | Analyse dependencies: direct runtime deps, structured groups, optional transitive graph (npm/pypi/hex/crates/vcpkg/zig/rubygems/go) | | `pkg changelog [spec]` | package spec OR `--repo-url` | `--from`, `--to`, `--limit`, `--git-ref`, `--no-body`, `--verbose`, `--json` | Release notes / changelog entries for a package or GitHub repo (GitHub Releases, CHANGELOG.md, or HexDocs). Default shows each entry with a 10-line body preview; `--verbose` uncaps, `--no-body` drops. | | `docs list ` | package spec (optional `@version`) | `--limit`, `--after`, `--verbose`, `--json` | List hosted/crawled and repository-backed documentation pages for a package. Entries include page IDs for `docs read`; JSON includes exact repo-file follow-up metadata when available. | | `docs read ` | page ID from `docs list` or search results | `--lines`, `--verbose`, `--json` | Read a documentation page by page ID. Default output is content-only; `--lines` fetches a bounded range for long pages. | @@ -124,7 +124,7 @@ githits pkg info npm:@types/node Shows a concise overview for a single package: latest version, license, description, repository + homepage, publication date, download count, GitHub metadata, install command, and known vulnerabilities. Default output is a compact terminal block. `--verbose` adds usage snippet, recent advisories, topics, and a recent-changes list. `--json` emits the lean hand-crafted envelope — every null field is omitted, every block that carries no actionable data is omitted entirely. -**Package spec.** `:`. Registries: `npm`, `pypi`, `hex`, `crates`, `nuget`, `maven`, `zig`, `vcpkg`, `packagist`. Scoped npm names (`npm:@types/node`) are supported. +**Package spec.** `:`. Registries: `npm`, `pypi`, `hex`, `crates`, `nuget`, `maven`, `zig`, `vcpkg`, `packagist`, `rubygems`, `go`. Scoped npm names (`npm:@types/node`) are supported. **Always latest.** `pkg info` returns the latest published version regardless of input. Passing `@` is rejected with `INVALID_ARGUMENT` and a clear message — the tool never silently swaps to latest. Use `pkg vulns` or `pkg deps` for version-pinned queries. @@ -182,9 +182,9 @@ githits pkg deps npm:express --transitive --depth 2 githits pkg deps npm:express --json ``` -Analyses dependencies for a package on npm, PyPI, Hex, Crates, vcpkg, or Zig. Default terminal output is a flat list of direct runtime dependencies with a hint summarising hidden groups. +Analyses dependencies for a package on npm, PyPI, Hex, Crates, vcpkg, Zig, RubyGems, or Go. Default terminal output is a flat list of direct runtime dependencies with a hint summarising hidden groups. -**Package spec.** `:[@]`. `@` is accepted (same as `pkg vulns`); defaults to latest. Tag-style inputs such as `@v4.18.0` are rejected client-side with `INVALID_ARGUMENT` — callers must use the canonical version. Only `npm`, `pypi`, `hex`, `crates`, `vcpkg`, and `zig` are supported; other registries are rejected client-side with `pkg deps only supports npm, pypi, hex, crates, vcpkg, and zig. Got: ${registry}.` +**Package spec.** `:[@]`. `@` is accepted (same as `pkg vulns`); defaults to latest. Tag-style inputs such as `@v4.18.0` are rejected client-side with `INVALID_ARGUMENT` — callers must use the canonical version. Only `npm`, `pypi`, `hex`, `crates`, `vcpkg`, `zig`, `rubygems`, and `go` are supported; other registries are rejected client-side with `pkg deps only supports npm, pypi, hex, crates, vcpkg, zig, rubygems, and go. Got: ${registry}.` **Two views.** The default runtime view collapses to a single-column list from `dependencies.direct` — the flat answer to "what does this pull in?". The structured groups view (`--groups`, or implicitly via `--lifecycle`) iterates `dependencyGroups.groups` and preserves registry-specific condition metadata (PyPI extras, Crates features). Dev / peer / build / optional deps live only in the groups view — the wire's `direct[]` is always runtime-only. diff --git a/docs/implementation/tools.md b/docs/implementation/tools.md index cd77cf0f..b82001b8 100644 --- a/docs/implementation/tools.md +++ b/docs/implementation/tools.md @@ -62,7 +62,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C **Omission rules.** Null scalars omitted; empty arrays dropped; zero-count `bySeverity` keys dropped; the `bySeverity` block itself dropped when `total === 0`. `modifiedAt` included only when it differs from `publishedAt`. `isMalicious` included only when `true`. -**Registry coverage.** Only npm, PyPI, Hex, and Crates have vulnerability data. The CLI + MCP reject the other five registries client-side with a tool-specific message (`pkg vulns only supports npm, pypi, hex, and crates. Got: ${registry}.`) — rejection predicate lives in `src/shared/package-vulnerabilities-request.ts` rather than the shared registry module, since it is a tool-specific capability matrix. +**Registry coverage.** Only npm, PyPI, Hex, and Crates have vulnerability data. The CLI + MCP reject the other registries client-side with a tool-specific message (`pkg vulns only supports npm, pypi, hex, and crates. Got: ${registry}.`) — rejection predicate lives in `src/shared/package-vulnerabilities-request.ts` rather than the shared registry module, since it is a tool-specific capability matrix. `pkg_vulns` shares its envelope builder and text formatter with the CLI `githits pkg vulns` command via `src/shared/package-vulnerabilities-request.ts` and `src/shared/package-vulnerabilities-response.ts`. MCP defaults to compact text and uses `format: "json"` for structured output. The parity test (`src/tools/package-vulnerabilities-parity.test.ts`) passes `format: "json"`, asserts `toEqual` across the service-sourced success and typed-error fixtures, and uses `toMatchObject` for builder-sourced `INVALID_ARGUMENT` fixtures such as unsupported registries and tag-style `v`-prefixed versions. @@ -80,7 +80,7 @@ The CLI mirrors the production MCP tool contract where equivalent tools exist. C **Typed decoder on GenericJSON payloads.** Backend declares `transitive.conflicts`, `transitive.circularDependencies`, and the DAG as `GenericJSON`. We ship best-effort decoders in the envelope builder that promote the two observed shapes (`{package_name, required_versions, conflicting_edges}` for conflicts; `{cycle: string[]}` for cycles) into typed arrays in the envelope. If any entry fails to decode against the expected shape, the field falls back to raw passthrough for that response — agents discriminate by checking `"name" in entry` / `Array.isArray(entry.cycle)` on the first element. `groups.environmentConstraints` remains raw `GenericJSON[]` (no observed live shape yet). The raw DAG is deliberately not exposed in this PR; a follow-up `pkg deps-dag` command will provide a typed graph surface for visualisation tooling. -**Registry coverage.** Only npm, PyPI, Hex, Crates, vcpkg, and Zig support the `packageDependencies` query. NuGet / Maven / Packagist are rejected client-side with a tool-specific message (`pkg deps only supports npm, pypi, hex, crates, vcpkg, and zig. Got: ${registry}.`). Predicate lives in `src/shared/package-dependencies-request.ts`. +**Registry coverage.** npm, PyPI, Hex, Crates, vcpkg, Zig, RubyGems, and Go support the `packageDependencies` query. NuGet / Maven / Packagist are rejected client-side with a tool-specific message (`pkg deps only supports npm, pypi, hex, crates, vcpkg, zig, rubygems, and go. Got: ${registry}.`). Predicate lives in `src/shared/package-dependencies-request.ts`. **Version validation.** Same rule as `pkg_vulns`: tag-style `v`-prefixed inputs are rejected client-side with `INVALID_ARGUMENT` before the backend call. diff --git a/src/commands/code/files.test.ts b/src/commands/code/files.test.ts index 542f9023..272083e0 100644 --- a/src/commands/code/files.test.ts +++ b/src/commands/code/files.test.ts @@ -213,6 +213,29 @@ describe("pkgFilesAction", () => { exitSpy.mockRestore(); }); + it.each([ + "rubygems:rails", + "go:golang.org/x/text", + ])("rejects package spec %s in --repo-url mode", async (spec) => { + const errorSpy = spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + try { + await pkgFilesAction( + spec, + undefined, + { repoUrl: "https://github.com/x/y", gitRef: "main" }, + createDeps(), + ); + } catch { + /* expected */ + } + expect(errorSpy.mock.calls[0]?.[0]).toContain("looks like a package spec"); + errorSpy.mockRestore(); + exitSpy.mockRestore(); + }); + it("emits the JSON envelope with --json", async () => { const logSpy = spyOn(console, "log").mockImplementation(() => {}); await pkgFilesAction( diff --git a/src/commands/code/files.ts b/src/commands/code/files.ts index 49c8c0f6..d1a00165 100644 --- a/src/commands/code/files.ts +++ b/src/commands/code/files.ts @@ -16,7 +16,11 @@ import { buildListFilesSuccessPayload, formatListFilesTerminal, } from "../../shared/list-files-response.js"; -import { toPkgseerRegistryLowercase } from "../../shared/pkgseer-registry.js"; +import { + PKGSEER_REGISTRY_ARGS, + PKGSEER_REGISTRY_LIST, + toPkgseerRegistryLowercase, +} from "../../shared/pkgseer-registry.js"; import { formatIndexingError, handleCodeNavCommandError, @@ -184,8 +188,10 @@ function buildCliListFilesParams( // a common user mistake where a package spec is passed together with // --repo-url. We'd otherwise silently treat it as a (meaningless) // path-prefix. -const REGISTRY_SPEC_HINT = - /^(npm|pypi|hex|crates|nuget|maven|zig|vcpkg|packagist):/i; +const REGISTRY_SPEC_HINT = new RegExp( + `^(${PKGSEER_REGISTRY_ARGS.join("|")}):`, + "i", +); function resolvePositionals( firstArg: string | undefined, @@ -224,8 +230,7 @@ and \`githits code grep\`. filters intersect on top. Addressing: (registry:name[@version]) OR --repo-url ---git-ref . Supported registries: npm, pypi, hex, crates, -vcpkg, zig, nuget, maven, packagist. +--git-ref . Supported registries: ${PKGSEER_REGISTRY_LIST}. By default each result is a bare path for easy piping; pass --verbose to include language / file-type / size annotations. diff --git a/src/commands/pkg/changelog.ts b/src/commands/pkg/changelog.ts index 44a61b66..38768b36 100644 --- a/src/commands/pkg/changelog.ts +++ b/src/commands/pkg/changelog.ts @@ -15,7 +15,10 @@ import { buildPackageChangelogSuccessPayload, formatPackageChangelogTerminal, } from "../../shared/package-changelog-response.js"; -import { toPkgseerRegistryLowercase } from "../../shared/pkgseer-registry.js"; +import { + PKGSEER_REGISTRY_LIST, + toPkgseerRegistryLowercase, +} from "../../shared/pkgseer-registry.js"; export interface PkgChangelogCommandOptions { repoUrl?: string; @@ -189,8 +192,7 @@ Addressing: (registry:name) OR --repo-url . Source (GitHub Releases, CHANGELOG.md, or HexDocs) is shown on the summary line. -Package spec: :. Supported registries: npm, pypi, -hex, crates, vcpkg, zig, nuget, maven, packagist. \`@\` +Package spec: :. Supported registries: ${PKGSEER_REGISTRY_LIST}. \`@\` is NOT accepted here — use --to for "entries up to this version".`; diff --git a/src/commands/pkg/deps.test.ts b/src/commands/pkg/deps.test.ts index 55e34448..002abd31 100644 --- a/src/commands/pkg/deps.test.ts +++ b/src/commands/pkg/deps.test.ts @@ -216,7 +216,7 @@ describe("pkgDepsAction", () => { } expect(errorSpy.mock.calls[0]?.[0]).toBe( - "pkg deps only supports npm, pypi, hex, crates, vcpkg, and zig. Got: nuget.", + "pkg deps only supports npm, pypi, hex, crates, vcpkg, zig, rubygems, and go. Got: nuget.", ); errorSpy.mockRestore(); exitSpy.mockRestore(); diff --git a/src/commands/pkg/deps.ts b/src/commands/pkg/deps.ts index e9306e28..d5dc8678 100644 --- a/src/commands/pkg/deps.ts +++ b/src/commands/pkg/deps.ts @@ -198,7 +198,7 @@ groups). Concrete --lifecycle values include runtime plus matching groups. conflict detection, and circular-dependency flags. Package spec: :[@]. Supported registries: -npm, pypi, hex, crates, vcpkg, zig. Omit @ for the latest +npm, pypi, hex, crates, vcpkg, zig, rubygems, go. Omit @ for the latest release.`; export function registerPkgDepsCommand(pkgCommand: Command): Command { diff --git a/src/commands/pkg/index.ts b/src/commands/pkg/index.ts index d0c161be..19d449a4 100644 --- a/src/commands/pkg/index.ts +++ b/src/commands/pkg/index.ts @@ -26,7 +26,7 @@ export async function registerPkgCommandGroup( .command("pkg") .summary("Package metadata: info, vulnerabilities, dependencies, changelog") .description( - "Inspect package metadata from npm, PyPI, Hex, Crates, NuGet, Maven, Packagist, vcpkg, and Zig: overviews, advisories, dependency graphs, and changelogs. For source-level operations inside a dependency, use `githits code`.", + "Inspect package metadata from npm, PyPI, Hex, Crates, NuGet, Maven, Packagist, RubyGems, Go, vcpkg, and Zig: overviews, advisories, dependency graphs, and changelogs. For source-level operations inside a dependency, use `githits code`.", ); registerPkgInfoCommand(pkgCommand); diff --git a/src/commands/pkg/info.ts b/src/commands/pkg/info.ts index b2a65422..63d49a7b 100644 --- a/src/commands/pkg/info.ts +++ b/src/commands/pkg/info.ts @@ -14,6 +14,7 @@ import { buildPackageSummarySuccessPayload, formatPackageSummaryTerminal, } from "../../shared/package-summary-response.js"; +import { PKGSEER_REGISTRY_LIST } from "../../shared/pkgseer-registry.js"; export interface PkgInfoCommandOptions { verbose?: boolean; @@ -103,8 +104,7 @@ repository, downloads, GitHub stars, install command, and known vulnerabilities. Use before picking a dependency or to orient on what a package is. -Package spec: :. Supported registries: npm, pypi, hex, -crates, nuget, maven, zig, vcpkg, packagist. +Package spec: :. Supported registries: ${PKGSEER_REGISTRY_LIST}. Always returns data for the latest published version.`; diff --git a/src/shared/list-package-docs-request.ts b/src/shared/list-package-docs-request.ts index 78df4871..2a4f2a55 100644 --- a/src/shared/list-package-docs-request.ts +++ b/src/shared/list-package-docs-request.ts @@ -5,6 +5,7 @@ import { } from "./package-spec.js"; import { isKnownPkgseerRegistryArg, + PKGSEER_REGISTRY_LIST, type PkgseerRegistryArg, toPkgseerRegistry, } from "./pkgseer-registry.js"; @@ -34,7 +35,7 @@ export function buildListPackageDocsParams( const registry = input.registry?.trim().toLowerCase() ?? ""; if (!isKnownPkgseerRegistryArg(registry)) { throw new UnsupportedRegistryError( - `Unsupported registry '${input.registry}'. Supported: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.`, + `Unsupported registry '${input.registry}'. Supported: ${PKGSEER_REGISTRY_LIST}.`, ); } diff --git a/src/shared/package-changelog-request.ts b/src/shared/package-changelog-request.ts index a4935bfb..a1f8bea4 100644 --- a/src/shared/package-changelog-request.ts +++ b/src/shared/package-changelog-request.ts @@ -29,6 +29,7 @@ import { } from "./package-spec.js"; import { isKnownPkgseerRegistryArg, + PKGSEER_REGISTRY_LIST, type PkgseerRegistryArg, toPkgseerRegistry, } from "./pkgseer-registry.js"; @@ -157,7 +158,7 @@ function resolveAddressing( const normalisedRegistryArg = input.registry?.trim().toLowerCase() ?? ""; if (!isKnownPkgseerRegistryArg(normalisedRegistryArg)) { throw new UnsupportedRegistryError( - `Unsupported registry '${input.registry}'. Supported: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.`, + `Unsupported registry '${input.registry}'. Supported: ${PKGSEER_REGISTRY_LIST}.`, ); } const registry = toPkgseerRegistry( diff --git a/src/shared/package-dependencies-request.test.ts b/src/shared/package-dependencies-request.test.ts index 5cc65fe9..2b71c021 100644 --- a/src/shared/package-dependencies-request.test.ts +++ b/src/shared/package-dependencies-request.test.ts @@ -16,6 +16,8 @@ describe("buildPackageDependenciesParams — registry matrix", () => { ["crates", "CRATES"], ["vcpkg", "VCPKG"], ["zig", "ZIG"], + ["rubygems", "RUBYGEMS"], + ["go", "GO"], ] as const)("accepts registry %s", (arg, expected) => { const result = buildPackageDependenciesParams({ registry: arg, diff --git a/src/shared/package-dependencies-request.ts b/src/shared/package-dependencies-request.ts index 103eaab8..a68496da 100644 --- a/src/shared/package-dependencies-request.ts +++ b/src/shared/package-dependencies-request.ts @@ -26,6 +26,7 @@ import { } from "./package-spec.js"; import { isKnownPkgseerRegistryArg, + PKGSEER_REGISTRY_LIST, type PkgseerRegistry, type PkgseerRegistryArg, toPkgseerRegistry, @@ -69,17 +70,19 @@ const LIFECYCLE_ORDER: Readonly> = { optional: 4, }; -const SUPPORTED_DEPS_REGISTRIES: ReadonlySet = new Set([ +export const SUPPORTED_DEPS_REGISTRIES: ReadonlySet = new Set([ "NPM", "PYPI", "HEX", "CRATES", "VCPKG", "ZIG", + "RUBYGEMS", + "GO", ]); -const SUPPORTED_DEPS_REGISTRIES_HUMAN = - "npm, pypi, hex, crates, vcpkg, and zig"; +export const SUPPORTED_DEPS_REGISTRIES_HUMAN = + "npm, pypi, hex, crates, vcpkg, zig, rubygems, and go"; export function supportsDependenciesRegistry( registry: PkgseerRegistry, @@ -129,7 +132,7 @@ export function buildPackageDependenciesParams( const normalisedRegistryArg = input.registry?.trim().toLowerCase() ?? ""; if (!isKnownPkgseerRegistryArg(normalisedRegistryArg)) { throw new UnsupportedRegistryError( - `Unsupported registry '${input.registry}'. Supported: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.`, + `Unsupported registry '${input.registry}'. Supported: ${PKGSEER_REGISTRY_LIST}.`, ); } diff --git a/src/shared/package-spec.test.ts b/src/shared/package-spec.test.ts index 9c79cfac..32f94ece 100644 --- a/src/shared/package-spec.test.ts +++ b/src/shared/package-spec.test.ts @@ -65,6 +65,21 @@ describe("parsePackageSpec", () => { }); }); + it("parses rubygems and go registry prefixes", () => { + expect(parsePackageSpec("rubygems:rails@8.0.2")).toEqual({ + registry: "rubygems", + registryExplicit: true, + name: "rails", + version: "8.0.2", + }); + expect(parsePackageSpec("go:golang.org/x/text@0.26.0")).toEqual({ + registry: "go", + registryExplicit: true, + name: "golang.org/x/text", + version: "0.26.0", + }); + }); + it("throws UnsupportedRegistryError when the prefix is unknown", () => { expect(() => parsePackageSpec("foobar:baz")).toThrow( UnsupportedRegistryError, diff --git a/src/shared/package-spec.ts b/src/shared/package-spec.ts index 25e7aefc..f04c14fe 100644 --- a/src/shared/package-spec.ts +++ b/src/shared/package-spec.ts @@ -1,17 +1,12 @@ +import { + PKGSEER_REGISTRY_ARGS, + PKGSEER_REGISTRY_LIST, +} from "./pkgseer-registry.js"; + /** * Known package registries supported by code navigation targets. */ -export const KNOWN_REGISTRIES = [ - "npm", - "pypi", - "hex", - "crates", - "nuget", - "maven", - "zig", - "vcpkg", - "packagist", -] as const; +export const KNOWN_REGISTRIES = PKGSEER_REGISTRY_ARGS; export type KnownRegistry = (typeof KNOWN_REGISTRIES)[number]; @@ -35,7 +30,7 @@ export interface ParsedPackageSpec { export class UnsupportedRegistryError extends Error { constructor(public readonly attempted: string) { super( - `Unsupported registry "${attempted}". Supported: ${KNOWN_REGISTRIES.join(", ")}.`, + `Unsupported registry "${attempted}". Supported: ${PKGSEER_REGISTRY_LIST}.`, ); this.name = "UnsupportedRegistryError"; } diff --git a/src/shared/package-summary-request.test.ts b/src/shared/package-summary-request.test.ts index 1427c986..4f9e9de7 100644 --- a/src/shared/package-summary-request.test.ts +++ b/src/shared/package-summary-request.test.ts @@ -4,6 +4,7 @@ import { UnsupportedRegistryError, } from "./package-spec.js"; import { buildPackageSummaryParams } from "./package-summary-request.js"; +import { knownPkgseerRegistryArgs } from "./pkgseer-registry.js"; describe("buildPackageSummaryParams", () => { it("maps lowercase registry to uppercase backend enum", () => { @@ -53,18 +54,7 @@ describe("buildPackageSummaryParams", () => { }); it("accepts every supported registry", () => { - const registries = [ - "npm", - "pypi", - "hex", - "crates", - "nuget", - "maven", - "zig", - "vcpkg", - "packagist", - ]; - for (const registry of registries) { + for (const registry of knownPkgseerRegistryArgs()) { expect(() => buildPackageSummaryParams({ registry, packageName: "x" }), ).not.toThrow(); diff --git a/src/shared/package-summary-request.ts b/src/shared/package-summary-request.ts index 06a519aa..eb48e32e 100644 --- a/src/shared/package-summary-request.ts +++ b/src/shared/package-summary-request.ts @@ -22,6 +22,7 @@ import { } from "./package-spec.js"; import { isKnownPkgseerRegistryArg, + PKGSEER_REGISTRY_LIST, type PkgseerRegistryArg, toPkgseerRegistry, } from "./pkgseer-registry.js"; @@ -48,7 +49,7 @@ export function buildPackageSummaryParams( const normalisedRegistry = input.registry?.trim().toLowerCase() ?? ""; if (!isKnownPkgseerRegistryArg(normalisedRegistry)) { throw new UnsupportedRegistryError( - `Unsupported registry '${input.registry}'. Supported: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.`, + `Unsupported registry '${input.registry}'. Supported: ${PKGSEER_REGISTRY_LIST}.`, ); } diff --git a/src/shared/package-vulnerabilities-request.test.ts b/src/shared/package-vulnerabilities-request.test.ts index 39a0abf4..97421cfe 100644 --- a/src/shared/package-vulnerabilities-request.test.ts +++ b/src/shared/package-vulnerabilities-request.test.ts @@ -150,7 +150,15 @@ describe("buildPackageVulnerabilitiesParams", () => { }); it("rejects known-but-unsupported registries with tool-specific message", () => { - const unsupported = ["vcpkg", "zig", "nuget", "maven", "packagist"]; + const unsupported = [ + "vcpkg", + "zig", + "nuget", + "maven", + "packagist", + "rubygems", + "go", + ]; for (const registry of unsupported) { try { buildPackageVulnerabilitiesParams({ @@ -188,12 +196,14 @@ describe("supportsVulnerabilitiesRegistry", () => { expect(supportsVulnerabilitiesRegistry("CRATES")).toBe(true); }); - it("rejects the five unsupported registries", () => { + it("rejects the unsupported registries", () => { expect(supportsVulnerabilitiesRegistry("NUGET")).toBe(false); expect(supportsVulnerabilitiesRegistry("MAVEN")).toBe(false); expect(supportsVulnerabilitiesRegistry("ZIG")).toBe(false); expect(supportsVulnerabilitiesRegistry("VCPKG")).toBe(false); expect(supportsVulnerabilitiesRegistry("PACKAGIST")).toBe(false); + expect(supportsVulnerabilitiesRegistry("RUBYGEMS")).toBe(false); + expect(supportsVulnerabilitiesRegistry("GO")).toBe(false); }); }); diff --git a/src/shared/package-vulnerabilities-request.ts b/src/shared/package-vulnerabilities-request.ts index be29242c..f32c2da2 100644 --- a/src/shared/package-vulnerabilities-request.ts +++ b/src/shared/package-vulnerabilities-request.ts @@ -6,7 +6,7 @@ * Responsibilities: * - Trim whitespace on `packageName`; reject empty strings with * `InvalidPackageSpecError`. - * - Normalise registry case and validate against the known 9-value + * - Normalise registry case and validate against the shared registry * surface via `pkgseer-registry`; reject unknown registries with * the generic `UnsupportedRegistryError` message. * - Gate against the vulnerability-query's narrower registry support @@ -35,6 +35,7 @@ import { } from "./package-spec.js"; import { isKnownPkgseerRegistryArg, + PKGSEER_REGISTRY_LIST, type PkgseerRegistry, type PkgseerRegistryArg, toPkgseerRegistry, @@ -116,7 +117,7 @@ export function buildPackageVulnerabilitiesParams( const normalisedRegistryArg = input.registry?.trim().toLowerCase() ?? ""; if (!isKnownPkgseerRegistryArg(normalisedRegistryArg)) { throw new UnsupportedRegistryError( - `Unsupported registry '${input.registry}'. Supported: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.`, + `Unsupported registry '${input.registry}'. Supported: ${PKGSEER_REGISTRY_LIST}.`, ); } diff --git a/src/shared/pkgseer-registry.test.ts b/src/shared/pkgseer-registry.test.ts index 182fb21e..d52fddc8 100644 --- a/src/shared/pkgseer-registry.test.ts +++ b/src/shared/pkgseer-registry.test.ts @@ -20,6 +20,8 @@ describe("toPkgseerRegistry", () => { ["zig", "ZIG"], ["vcpkg", "VCPKG"], ["packagist", "PACKAGIST"], + ["rubygems", "RUBYGEMS"], + ["go", "GO"], ]; for (const [arg, expected] of cases) { diff --git a/src/shared/pkgseer-registry.ts b/src/shared/pkgseer-registry.ts index 5d6bbc5a..34a6986e 100644 --- a/src/shared/pkgseer-registry.ts +++ b/src/shared/pkgseer-registry.ts @@ -19,7 +19,25 @@ export type PkgseerRegistry = | "MAVEN" | "ZIG" | "VCPKG" - | "PACKAGIST"; + | "PACKAGIST" + | "RUBYGEMS" + | "GO"; + +export const PKGSEER_REGISTRY_ARGS = [ + "npm", + "pypi", + "hex", + "crates", + "nuget", + "maven", + "zig", + "vcpkg", + "packagist", + "rubygems", + "go", +] as const; + +export type PkgseerRegistryArg = (typeof PKGSEER_REGISTRY_ARGS)[number]; const registryMap = { npm: "NPM", @@ -31,9 +49,11 @@ const registryMap = { zig: "ZIG", vcpkg: "VCPKG", packagist: "PACKAGIST", -} as const satisfies Record; + rubygems: "RUBYGEMS", + go: "GO", +} as const satisfies Record; -export type PkgseerRegistryArg = keyof typeof registryMap; +export const PKGSEER_REGISTRY_LIST = PKGSEER_REGISTRY_ARGS.join(", "); /** * Lowercase surface value → uppercase backend enum. Exhaustive over @@ -69,5 +89,5 @@ export function isKnownPkgseerRegistryArg( } export function knownPkgseerRegistryArgs(): ReadonlyArray { - return Object.keys(registryMap) as PkgseerRegistryArg[]; + return PKGSEER_REGISTRY_ARGS; } diff --git a/src/tools/code-navigation-shared.ts b/src/tools/code-navigation-shared.ts index e1f6747e..4147568e 100644 --- a/src/tools/code-navigation-shared.ts +++ b/src/tools/code-navigation-shared.ts @@ -6,6 +6,10 @@ import { } from "../shared/code-navigation.js"; import { mapCodeNavigationError } from "../shared/code-navigation-error-map.js"; import { parseCodeNavigationTargetSpec } from "../shared/code-navigation-target.js"; +import { + PKGSEER_REGISTRY_ARGS, + PKGSEER_REGISTRY_LIST, +} from "../shared/pkgseer-registry.js"; import { errorResult, type ToolResult } from "./types.js"; // Re-export the wait-timeout default so callers already importing this @@ -16,20 +20,10 @@ export { DEFAULT_WAIT_TIMEOUT_MS } from "../shared/code-navigation-defaults.js"; export const structuredCodeTargetSchema = z .object({ registry: z - .enum([ - "npm", - "pypi", - "hex", - "crates", - "nuget", - "maven", - "zig", - "vcpkg", - "packagist", - ]) + .enum(PKGSEER_REGISTRY_ARGS) .optional() .describe( - "Package registry (npm, pypi, hex, etc.). Required for package scope.", + `Package registry (${PKGSEER_REGISTRY_LIST}). Required for package scope.`, ), package_name: z .string() diff --git a/src/tools/grep-repo-parity.test.ts b/src/tools/grep-repo-parity.test.ts index 46d5a9b6..20fe7111 100644 --- a/src/tools/grep-repo-parity.test.ts +++ b/src/tools/grep-repo-parity.test.ts @@ -72,7 +72,9 @@ interface McpArgs { | "maven" | "zig" | "vcpkg" - | "packagist"; + | "packagist" + | "rubygems" + | "go"; package_name?: string; version?: string; repo_url?: string; diff --git a/src/tools/list-files-parity.test.ts b/src/tools/list-files-parity.test.ts index c72bb87e..64ac7968 100644 --- a/src/tools/list-files-parity.test.ts +++ b/src/tools/list-files-parity.test.ts @@ -77,7 +77,9 @@ interface McpArgs { | "maven" | "zig" | "vcpkg" - | "packagist"; + | "packagist" + | "rubygems" + | "go"; package_name?: string; version?: string; repo_url?: string; diff --git a/src/tools/list-package-docs.ts b/src/tools/list-package-docs.ts index 921ea707..e7390146 100644 --- a/src/tools/list-package-docs.ts +++ b/src/tools/list-package-docs.ts @@ -4,6 +4,7 @@ import { buildListPackageDocsParams } from "../shared/list-package-docs-request. import { buildListPackageDocsSuccessPayload } from "../shared/list-package-docs-response.js"; import { renderListPackageDocsText } from "../shared/list-package-docs-text.js"; import { mapPackageIntelligenceError } from "../shared/package-intelligence-error-map.js"; +import { PKGSEER_REGISTRY_LIST } from "../shared/pkgseer-registry.js"; import { errorResult, type ToolDefinition, textResult } from "./types.js"; export interface ListPackageDocsArgs { @@ -18,9 +19,7 @@ export interface ListPackageDocsArgs { const schema = { registry: z .string() - .describe( - "Package registry. One of: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.", - ), + .describe(`Package registry. One of: ${PKGSEER_REGISTRY_LIST}.`), package_name: z .string() .describe("Package name (scoped names ok: @types/node)."), diff --git a/src/tools/package-changelog.ts b/src/tools/package-changelog.ts index e492ab52..9fa2c66e 100644 --- a/src/tools/package-changelog.ts +++ b/src/tools/package-changelog.ts @@ -6,7 +6,10 @@ import { formatPackageChangelogTerminal, } from "../shared/package-changelog-response.js"; import { mapPackageIntelligenceError } from "../shared/package-intelligence-error-map.js"; -import { toPkgseerRegistryLowercase } from "../shared/pkgseer-registry.js"; +import { + PKGSEER_REGISTRY_LIST, + toPkgseerRegistryLowercase, +} from "../shared/pkgseer-registry.js"; import { type ToolDefinition, textResult } from "./types.js"; export interface PackageChangelogArgs { @@ -41,7 +44,7 @@ const schema = { .string() .optional() .describe( - "Package registry (with `package_name`). Mutually exclusive with `repo_url`. Supported: npm, pypi, hex, crates, vcpkg, zig, nuget, maven, packagist.", + `Package registry (with \`package_name\`). Mutually exclusive with \`repo_url\`. Supported: ${PKGSEER_REGISTRY_LIST}.`, ), package_name: z .string() @@ -107,7 +110,7 @@ const DESCRIPTION = "`include_bodies: false` for a version / date / URL timeline only. " + "Package-version entries without changelog text succeed with `source` " + "omitted; no-source plus no entries returns `NOT_FOUND`. Supports npm, " + - "PyPI, Hex, Crates, vcpkg, Zig, NuGet, Maven, Packagist."; + "PyPI, Hex, Crates, vcpkg, Zig, NuGet, Maven, Packagist, RubyGems, Go."; export function createPackageChangelogTool( service: PackageIntelligenceService, diff --git a/src/tools/package-dependencies.test.ts b/src/tools/package-dependencies.test.ts index b9d18864..89180315 100644 --- a/src/tools/package-dependencies.test.ts +++ b/src/tools/package-dependencies.test.ts @@ -292,7 +292,7 @@ describe("createPackageDependenciesTool — validation errors via in-handler bui const payload = parseText(result) as { code: string; error: string }; expect(payload.code).toBe("INVALID_ARGUMENT"); expect(payload.error).toBe( - "pkg deps only supports npm, pypi, hex, crates, vcpkg, and zig. Got: nuget.", + "pkg deps only supports npm, pypi, hex, crates, vcpkg, zig, rubygems, and go. Got: nuget.", ); }); diff --git a/src/tools/package-dependencies.ts b/src/tools/package-dependencies.ts index 17540307..167f7fd1 100644 --- a/src/tools/package-dependencies.ts +++ b/src/tools/package-dependencies.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import type { PackageIntelligenceService } from "../services/index.js"; import { InvalidPackageSpecError } from "../shared/index.js"; -import { buildPackageDependenciesParams } from "../shared/package-dependencies-request.js"; +import { + buildPackageDependenciesParams, + SUPPORTED_DEPS_REGISTRIES_HUMAN, +} from "../shared/package-dependencies-request.js"; import { buildPackageDependenciesSuccessPayload, formatPackageDependenciesTerminal, @@ -33,7 +36,7 @@ const schema = { registry: z .string() .describe( - "Package registry. Dependency data is available on npm, pypi, hex, crates, vcpkg, and zig.", + `Package registry. Dependency data is available on ${SUPPORTED_DEPS_REGISTRIES_HUMAN}.`, ), package_name: z .string() @@ -90,7 +93,7 @@ const DESCRIPTION = "full install footprint, conflict detection, and circular-" + "dependency flags; layer `include_importers: true` on top when you " + "also need per-package provenance. Supports npm, PyPI, Hex, Crates, " + - "vcpkg, and Zig."; + "RubyGems, Go, vcpkg, and Zig."; export function createPackageDependenciesTool( service: PackageIntelligenceService, diff --git a/src/tools/package-summary.ts b/src/tools/package-summary.ts index c9c7fcac..ab27c23c 100644 --- a/src/tools/package-summary.ts +++ b/src/tools/package-summary.ts @@ -6,6 +6,7 @@ import { buildPackageSummarySuccessPayload, formatPackageSummaryTerminal, } from "../shared/package-summary-response.js"; +import { PKGSEER_REGISTRY_LIST } from "../shared/pkgseer-registry.js"; import { type ToolDefinition, textResult } from "./types.js"; export interface PackageSummaryArgs { @@ -24,9 +25,7 @@ export interface PackageSummaryArgs { const schema = { registry: z .string() - .describe( - "Package registry. One of: npm, pypi, hex, crates, nuget, maven, zig, vcpkg, packagist.", - ), + .describe(`Package registry. One of: ${PKGSEER_REGISTRY_LIST}.`), package_name: z .string() .describe("Package name (scoped names ok: @types/node)."), @@ -46,7 +45,7 @@ const DESCRIPTION = 'Default output is compact text; pass `format: "json"` for the ' + "structured envelope. " + "Works across npm, PyPI, Hex, Crates, NuGet, Maven, Packagist, " + - "vcpkg, and Zig. Always returns data for the latest published " + + "RubyGems, Go, vcpkg, and Zig. Always returns data for the latest published " + "version."; export function createPackageSummaryTool( diff --git a/src/tools/read-file-parity.test.ts b/src/tools/read-file-parity.test.ts index 11bb320e..f7cabd9c 100644 --- a/src/tools/read-file-parity.test.ts +++ b/src/tools/read-file-parity.test.ts @@ -70,7 +70,9 @@ interface McpArgs { | "maven" | "zig" | "vcpkg" - | "packagist"; + | "packagist" + | "rubygems" + | "go"; package_name?: string; version?: string; repo_url?: string;