Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/implementation/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The CLI exposes `example`, `languages`, `feedback`, top-level indexed `search` /
| `feedback <solution_id>` | `--accept` or `--reject` | `-m, --message <text>`, `--json` | Submit feedback on a search result |
| `pkg info <spec>` | package spec | `--verbose`, `--json` | Show a package overview (latest version, downloads, license, vulnerabilities) |
| `pkg vulns <spec>` | package spec (optional `@version`) | `--severity`, `--include-withdrawn`, `--verbose`, `--json` | List known vulnerabilities for a package (npm/pypi/hex/crates) |
| `pkg deps <spec>` | 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 <spec>` | 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 <spec>` | 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>` | 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. |
Expand Down Expand Up @@ -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.** `<registry>:<name>`. Registries: `npm`, `pypi`, `hex`, `crates`, `nuget`, `maven`, `zig`, `vcpkg`, `packagist`. Scoped npm names (`npm:@types/node`) are supported.
**Package spec.** `<registry>:<name>`. 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 `<spec>@<version>` 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.

Expand Down Expand Up @@ -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.** `<registry>:<name>[@<version>]`. `@<version>` 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.** `<registry>:<name>[@<version>]`. `@<version>` 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.

Expand Down
4 changes: 2 additions & 2 deletions docs/implementation/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down
23 changes: 23 additions & 0 deletions src/commands/code/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 10 additions & 5 deletions src/commands/code/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -224,8 +230,7 @@ and \`githits code grep\`.
filters intersect on top.

Addressing: <spec> (registry:name[@version]) OR --repo-url <url>
--git-ref <ref>. Supported registries: npm, pypi, hex, crates,
vcpkg, zig, nuget, maven, packagist.
--git-ref <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.
Expand Down
8 changes: 5 additions & 3 deletions src/commands/pkg/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -189,8 +192,7 @@ Addressing: <spec> (registry:name) OR --repo-url <url>. Source
(GitHub Releases, CHANGELOG.md, or HexDocs) is shown on the summary
line.

Package spec: <registry>:<name>. Supported registries: npm, pypi,
hex, crates, vcpkg, zig, nuget, maven, packagist. \`<spec>@<version>\`
Package spec: <registry>:<name>. Supported registries: ${PKGSEER_REGISTRY_LIST}. \`<spec>@<version>\`
is NOT accepted here — use --to <version> for "entries up to this
version".`;

Expand Down
2 changes: 1 addition & 1 deletion src/commands/pkg/deps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/commands/pkg/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ groups). Concrete --lifecycle values include runtime plus matching groups.
conflict detection, and circular-dependency flags.

Package spec: <registry>:<name>[@<version>]. Supported registries:
npm, pypi, hex, crates, vcpkg, zig. Omit @<version> for the latest
npm, pypi, hex, crates, vcpkg, zig, rubygems, go. Omit @<version> for the latest
release.`;

export function registerPkgDepsCommand(pkgCommand: Command): Command {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/pkg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/pkg/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: <registry>:<name>. Supported registries: npm, pypi, hex,
crates, nuget, maven, zig, vcpkg, packagist.
Package spec: <registry>:<name>. Supported registries: ${PKGSEER_REGISTRY_LIST}.

Always returns data for the latest published version.`;

Expand Down
3 changes: 2 additions & 1 deletion src/shared/list-package-docs-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "./package-spec.js";
import {
isKnownPkgseerRegistryArg,
PKGSEER_REGISTRY_LIST,
type PkgseerRegistryArg,
toPkgseerRegistry,
} from "./pkgseer-registry.js";
Expand Down Expand Up @@ -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}.`,
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/shared/package-changelog-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from "./package-spec.js";
import {
isKnownPkgseerRegistryArg,
PKGSEER_REGISTRY_LIST,
type PkgseerRegistryArg,
toPkgseerRegistry,
} from "./pkgseer-registry.js";
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/shared/package-dependencies-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions src/shared/package-dependencies-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from "./package-spec.js";
import {
isKnownPkgseerRegistryArg,
PKGSEER_REGISTRY_LIST,
type PkgseerRegistry,
type PkgseerRegistryArg,
toPkgseerRegistry,
Expand Down Expand Up @@ -69,17 +70,19 @@ const LIFECYCLE_ORDER: Readonly<Record<DependencyLifecycle, number>> = {
optional: 4,
};

const SUPPORTED_DEPS_REGISTRIES: ReadonlySet<PkgseerRegistry> = new Set([
export const SUPPORTED_DEPS_REGISTRIES: ReadonlySet<PkgseerRegistry> = 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,
Expand Down Expand Up @@ -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}.`,
);
}

Expand Down
15 changes: 15 additions & 0 deletions src/shared/package-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 7 additions & 12 deletions src/shared/package-spec.ts
Original file line number Diff line number Diff line change
@@ -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];

Expand All @@ -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";
}
Expand Down
14 changes: 2 additions & 12 deletions src/shared/package-summary-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion src/shared/package-summary-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from "./package-spec.js";
import {
isKnownPkgseerRegistryArg,
PKGSEER_REGISTRY_LIST,
type PkgseerRegistryArg,
toPkgseerRegistry,
} from "./pkgseer-registry.js";
Expand All @@ -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}.`,
);
}

Expand Down
Loading
Loading