Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/commands/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ function getRegistryLabel(registry: Registry): string {
return "PyPI";
case "crates":
return "crates.io";
case "packagist":
return "Packagist";
}
}

Expand Down
11 changes: 7 additions & 4 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const REGISTRY_LABELS: Record<Registry, string> = {
npm: "npm",
pypi: "PyPI",
crates: "crates.io",
packagist: "Packagist",
};

/**
Expand All @@ -28,9 +29,10 @@ export async function listCommand(options: ListOptions = {}): Promise<void> {
);
console.log("Use `opensrc <owner>/<repo>` to fetch a GitHub repository.");
console.log("\nSupported registries:");
console.log(" • npm: opensrc zod, opensrc npm:react");
console.log(" • PyPI: opensrc pypi:requests");
console.log(" • crates: opensrc crates:serde");
console.log(" • npm: opensrc zod, opensrc npm:react");
console.log(" • PyPI: opensrc pypi:requests");
console.log(" • crates: opensrc crates:serde");
console.log(" • Packagist: opensrc packagist:laravel/framework");
return;
}

Expand All @@ -44,14 +46,15 @@ export async function listCommand(options: ListOptions = {}): Promise<void> {
npm: [],
pypi: [],
crates: [],
packagist: [],
};

for (const pkg of sources.packages) {
packagesByRegistry[pkg.registry].push(pkg);
}

// Display packages by registry
const registries: Registry[] = ["npm", "pypi", "crates"];
const registries: Registry[] = ["npm", "pypi", "crates", "packagist"];
let hasDisplayedPackages = false;

for (const registry of registries) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export async function removeCommand(

if (!pkgInfo) {
// Try other registries if default didn't work
const registries: Registry[] = ["npm", "pypi", "crates"];
const registries: Registry[] = ["npm", "pypi", "crates", "packagist"];
for (const reg of registries) {
if (reg !== registry) {
pkgInfo = await getPackageInfo(cleanSpec, cwd, reg);
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ program
program
.argument(
"[packages...]",
"packages or repos to fetch (e.g., zod, pypi:requests, crates:serde, owner/repo)",
"packages or repos to fetch (e.g., zod, pypi:requests, crates:serde, packagist:laravel/framework, owner/repo)",
)
.option("--cwd <path>", "working directory (default: current directory)")
.option(
Expand Down Expand Up @@ -80,6 +80,7 @@ program
.option("--npm", "only remove npm packages")
.option("--pypi", "only remove PyPI packages")
.option("--crates", "only remove crates.io packages")
.option("--packagist", "only remove Packagist packages")
.option("--cwd <path>", "working directory (default: current directory)")
.action(
async (options: {
Expand All @@ -88,13 +89,15 @@ program
npm?: boolean;
pypi?: boolean;
crates?: boolean;
packagist?: boolean;
cwd?: string;
}) => {
// Determine registry from flags
let registry: Registry | undefined;
if (options.npm) registry = "npm";
else if (options.pypi) registry = "pypi";
else if (options.crates) registry = "crates";
else if (options.packagist) registry = "packagist";

await cleanCommand({
packages: options.packages || !!registry,
Expand Down
9 changes: 5 additions & 4 deletions src/lib/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ Use this source code when you need to understand how a package works internally,
To fetch source code for a package or repository you need to understand, run:

\`\`\`bash
npx opensrc <package> # npm package (e.g., npx opensrc zod)
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
npx opensrc <package> # npm package (e.g., npx opensrc zod)
npx opensrc pypi:<package> # Python package (e.g., npx opensrc pypi:requests)
npx opensrc crates:<package> # Rust crate (e.g., npx opensrc crates:serde)
npx opensrc packagist:<package> # PHP package (e.g., npx opensrc packagist:laravel/framework)
npx opensrc <owner>/<repo> # GitHub repo (e.g., npx opensrc vercel/ai)
\`\`\`

${SECTION_END_MARKER}`;
Expand Down
67 changes: 67 additions & 0 deletions src/lib/registries/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,36 @@ describe("detectRegistry", () => {
});
});

describe("packagist registry", () => {
it("detects packagist: prefix", () => {
expect(detectRegistry("packagist:laravel/framework")).toEqual({
registry: "packagist",
cleanSpec: "laravel/framework",
});
});

it("detects composer: prefix", () => {
expect(detectRegistry("composer:laravel/framework")).toEqual({
registry: "packagist",
cleanSpec: "laravel/framework",
});
});

it("detects php: prefix", () => {
expect(detectRegistry("php:symfony/symfony")).toEqual({
registry: "packagist",
cleanSpec: "symfony/symfony",
});
});

it("handles case-insensitive prefixes", () => {
expect(detectRegistry("PACKAGIST:laravel/framework")).toEqual({
registry: "packagist",
cleanSpec: "laravel/framework",
});
});
});

describe("preserves version in cleanSpec", () => {
it("npm with version", () => {
expect(detectRegistry("npm:lodash@4.17.21")).toEqual({
Expand All @@ -106,6 +136,13 @@ describe("detectRegistry", () => {
cleanSpec: "serde@1.0.0",
});
});

it("packagist with version", () => {
expect(detectRegistry("packagist:laravel/framework@11.0.0")).toEqual({
registry: "packagist",
cleanSpec: "laravel/framework@11.0.0",
});
});
});
});

Expand Down Expand Up @@ -179,6 +216,24 @@ describe("parsePackageSpec", () => {
});
});
});

describe("packagist packages", () => {
it("parses packagist package", () => {
expect(parsePackageSpec("packagist:laravel/framework")).toEqual({
registry: "packagist",
name: "laravel/framework",
version: undefined,
});
});

it("parses packagist package with @ version", () => {
expect(parsePackageSpec("php:laravel/framework@11.0.0")).toEqual({
registry: "packagist",
name: "laravel/framework",
version: "11.0.0",
});
});
});
});

describe("detectInputType", () => {
Expand All @@ -203,6 +258,18 @@ describe("detectInputType", () => {
expect(detectInputType("crates:serde")).toBe("package");
});

it("packagist package", () => {
expect(detectInputType("packagist:laravel/framework")).toBe("package");
});

it("composer package", () => {
expect(detectInputType("composer:symfony/symfony")).toBe("package");
});

it("php package", () => {
expect(detectInputType("php:guzzlehttp/guzzle")).toBe("package");
});

it("package with version", () => {
expect(detectInputType("lodash@4.17.21")).toBe("package");
});
Expand Down
10 changes: 10 additions & 0 deletions src/lib/registries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { Registry, PackageSpec, ResolvedPackage } from "../../types.js";
import { parseNpmSpec, resolveNpmPackage } from "./npm.js";
import { parsePyPISpec, resolvePyPIPackage } from "./pypi.js";
import { parseCratesSpec, resolveCrate } from "./crates.js";
import { parsePackagistSpec, resolvePackagistPackage } from "./packagist.js";
import { isRepoSpec } from "../repo.js";

export { resolveNpmPackage } from "./npm.js";
export { resolvePyPIPackage } from "./pypi.js";
export { resolveCrate } from "./crates.js";
export { resolvePackagistPackage } from "./packagist.js";

/**
* Registry prefixes for explicit specification
Expand All @@ -19,6 +21,9 @@ const REGISTRY_PREFIXES: Record<string, Registry> = {
"crates:": "crates",
"cargo:": "crates",
"rust:": "crates",
"packagist:": "packagist",
"composer:": "packagist",
"php:": "packagist",
};

/**
Expand Down Expand Up @@ -67,6 +72,9 @@ export function parsePackageSpec(spec: string): PackageSpec {
case "crates":
({ name, version } = parseCratesSpec(cleanSpec));
break;
case "packagist":
({ name, version } = parsePackagistSpec(cleanSpec));
break;
}

return { registry, name, version };
Expand All @@ -87,6 +95,8 @@ export async function resolvePackage(
return resolvePyPIPackage(name, version);
case "crates":
return resolveCrate(name, version);
case "packagist":
return resolvePackagistPackage(name, version);
}
}

Expand Down
87 changes: 87 additions & 0 deletions src/lib/registries/packagist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect } from "vitest";
import { parsePackagistSpec } from "./packagist.js";

describe("parsePackagistSpec", () => {
describe("package name only", () => {
it("parses simple vendor/package name", () => {
expect(parsePackagistSpec("laravel/framework")).toEqual({
name: "laravel/framework",
version: undefined,
});
});

it("parses package with hyphens", () => {
expect(parsePackagistSpec("symfony/http-foundation")).toEqual({
name: "symfony/http-foundation",
version: undefined,
});
});

it("parses package with underscores", () => {
expect(parsePackagistSpec("doctrine/dbal")).toEqual({
name: "doctrine/dbal",
version: undefined,
});
});
});

describe("@ version specifier", () => {
it("parses package@version", () => {
expect(parsePackagistSpec("laravel/framework@11.0.0")).toEqual({
name: "laravel/framework",
version: "11.0.0",
});
});

it("parses complex package name@version", () => {
expect(parsePackagistSpec("symfony/http-foundation@7.0.0")).toEqual({
name: "symfony/http-foundation",
version: "7.0.0",
});
});

it("parses with v prefix in version", () => {
expect(parsePackagistSpec("guzzlehttp/guzzle@v7.8.0")).toEqual({
name: "guzzlehttp/guzzle",
version: "v7.8.0",
});
});
});

describe("edge cases", () => {
it("handles whitespace trimming", () => {
expect(parsePackagistSpec(" laravel/framework ")).toEqual({
name: "laravel/framework",
version: undefined,
});
});

it("handles dev versions", () => {
expect(parsePackagistSpec("laravel/framework@dev-master")).toEqual({
name: "laravel/framework",
version: "dev-master",
});
});

it("handles prerelease versions", () => {
expect(parsePackagistSpec("laravel/framework@11.0.0-alpha.1")).toEqual({
name: "laravel/framework",
version: "11.0.0-alpha.1",
});
});

it("handles beta versions", () => {
expect(parsePackagistSpec("symfony/symfony:7.0.0-beta1")).toEqual({
name: "symfony/symfony",
version: "7.0.0-beta1",
});
});

it("handles package without vendor prefix as-is", () => {
expect(parsePackagistSpec("somepackage")).toEqual({
name: "somepackage",
version: undefined,
});
});
});
});
Loading