[{"data":1,"prerenderedAt":1282},["ShallowReactive",2],{"NoscriptNav_XrRK2e2e8meJ0jKVGkb5ULGQDVi3UiFQ9nupAr7Yns":3,"\u002Ftools\u002Fgo-modules-for-package-management-tooling":8},["Island",4],{"key":5,"result":6},"NoscriptNav_XrRK2e2e8meJ0jKVGkb5ULGQDVi3UiFQ9nupAr7Yns",{"head":7},{},{"id":9,"title":10,"authors":11,"body":13,"canonicalUrl":1270,"canonicalWebsiteName":1271,"category":1118,"date":1272,"description":1273,"extension":1274,"featured":1275,"fullWidthLayout":1275,"image":1276,"imageAlt":1276,"location":1276,"meta":1277,"metaImage":1276,"navigation":112,"path":1278,"seo":1279,"stem":1280,"venue":1276,"venueUrl":1276,"__hash__":1281},"tools\u002Ftools\u002Fgo-modules-for-package-management-tooling.md","Go Modules for Package Management Tooling",[12],"andrew",{"type":14,"value":15,"toc":1246},"minimark",[16,28,56,61,69,83,143,150,170,173,212,219,222,265,272,296,345,349,356,365,449,452,459,468,520,527,559,642,649,687,736,739,743,750,753,796,799,806,824,893,900,912,964,971,980,1027,1034,1046,1116,1120,1127,1130,1195,1209,1216,1219,1234,1242],[17,18,19,20,27],"p",{},"I've been working on a reusable layer for building ecosystem-agnostic package and supply chain tools in Go: fourteen modules under ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs",[25],"nofollow","git-pkgs"," covering manifest parsing, registry clients, license normalization, platform translation, vulnerability feeds, and more.",[17,29,30,31,36,37,42,43,47,48,51,52,55],{},"These are rebuilds of libraries I've written and used in Ruby for years, some going back to ",[21,32,35],{"href":33,"rel":34},"https:\u002F\u002Flibraries.io",[25],"Libraries.io"," and more recently for ",[21,38,41],{"href":39,"rel":40},"https:\u002F\u002Fecosyste.ms",[25],"Ecosyste.ms",", which I wrote about ",[21,44,46],{"href":45},"\u002Freports\u002Fsupply-chain-security-tools-for-ruby","previously",". I built the Go versions for ",[21,49,26],{"href":50},"\u002Fideas\u002Frewriting-git-pkgs-in-go",", a tool for exploring the dependency history of your repositories that ",[21,53,54],{"href":50},"compiles to a single binary"," with no runtime dependencies, which matters for a git subcommand that needs to just work on any machine. When I went looking for Go equivalents of my Ruby libraries, most were either abandoned, incomplete, or only covered a single ecosystem, so I rebuilt them.",[57,58,60],"h2",{"id":59},"identification","Identification",[62,63,65],"h3",{"id":64},"purl",[21,66,64],{"href":67,"rel":68},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fpurl",[25],[17,70,71,76,77,82],{},[21,72,75],{"href":73,"rel":74},"https:\u002F\u002Fgithub.com\u002Fpackage-url\u002Fpurl-spec",[25],"Package URL"," (now ",[21,78,81],{"href":79,"rel":80},"https:\u002F\u002Fecma-international.org\u002Fpublications-and-standards\u002Fstandards\u002Fecma-427\u002F",[25],"ECMA-427",") is the standard format for identifying packages across ecosystems. This handles parsing, generation, and type-specific configuration for around 40 ecosystems, including registry URL generation and the reverse: parsing a registry URL back into a PURL.",[84,85,90],"pre",{"className":86,"code":87,"language":88,"meta":89,"style":89},"language-go shiki shiki-themes github-light github-dark","p, _ := purl.Parse(\"pkg:npm\u002F%40babel\u002Fcore@7.24.0\")\np.FullName()  \u002F\u002F \"@babel\u002Fcore\"\n\nurl, _ := p.RegistryURL()  \u002F\u002F \"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@babel\u002Fcore\"\n\n\u002F\u002F Reverse lookup\np, _ = purl.ParseRegistryURL(\"https:\u002F\u002Fcrates.io\u002Fcrates\u002Fserde\")\np.String()  \u002F\u002F \"pkg:cargo\u002Fserde\"\n","go","",[91,92,93,101,107,114,120,125,131,137],"code",{"__ignoreMap":89},[94,95,98],"span",{"class":96,"line":97},"line",1,[94,99,100],{},"p, _ := purl.Parse(\"pkg:npm\u002F%40babel\u002Fcore@7.24.0\")\n",[94,102,104],{"class":96,"line":103},2,[94,105,106],{},"p.FullName()  \u002F\u002F \"@babel\u002Fcore\"\n",[94,108,110],{"class":96,"line":109},3,[94,111,113],{"emptyLinePlaceholder":112},true,"\n",[94,115,117],{"class":96,"line":116},4,[94,118,119],{},"url, _ := p.RegistryURL()  \u002F\u002F \"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@babel\u002Fcore\"\n",[94,121,123],{"class":96,"line":122},5,[94,124,113],{"emptyLinePlaceholder":112},[94,126,128],{"class":96,"line":127},6,[94,129,130],{},"\u002F\u002F Reverse lookup\n",[94,132,134],{"class":96,"line":133},7,[94,135,136],{},"p, _ = purl.ParseRegistryURL(\"https:\u002F\u002Fcrates.io\u002Fcrates\u002Fserde\")\n",[94,138,140],{"class":96,"line":139},8,[94,141,142],{},"p.String()  \u002F\u002F \"pkg:cargo\u002Fserde\"\n",[62,144,146],{"id":145},"vers",[21,147,145],{"href":148,"rel":149},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fvers",[25],[17,151,152,157,158,161,162,165,166,169],{},[21,153,156],{"href":154,"rel":155},"https:\u002F\u002Fgithub.com\u002Fpackage-url\u002Fvers-spec",[25],"VERS"," is the version range specification that accompanies PURL. Different ecosystems have incompatible range syntaxes: npm uses ",[91,159,160],{},"^1.2.3",", Ruby uses ",[91,163,164],{},"~> 1.2",", Maven uses ",[91,167,168],{},"[1.0,2.0)",". VERS provides one syntax to normalize everything to.",[17,171,172],{},"It parses both VERS URIs and native ecosystem syntax, using a mathematical interval model internally to check whether a given version falls within a range:",[84,174,176],{"className":86,"code":175,"language":88,"meta":89,"style":89},"r, _ := vers.Parse(\"vers:npm\u002F>=1.0.0|\u003C2.0.0\")\nr.Contains(\"1.5.0\")  \u002F\u002F true\n\n\u002F\u002F Native ecosystem syntax works too\nr, _ = vers.ParseNative(\"~> 1.2.3\", \"gem\")\nr.Contains(\"1.2.5\")  \u002F\u002F true\nr.Contains(\"1.3.0\")  \u002F\u002F false\n",[91,177,178,183,188,192,197,202,207],{"__ignoreMap":89},[94,179,180],{"class":96,"line":97},[94,181,182],{},"r, _ := vers.Parse(\"vers:npm\u002F>=1.0.0|\u003C2.0.0\")\n",[94,184,185],{"class":96,"line":103},[94,186,187],{},"r.Contains(\"1.5.0\")  \u002F\u002F true\n",[94,189,190],{"class":96,"line":109},[94,191,113],{"emptyLinePlaceholder":112},[94,193,194],{"class":96,"line":116},[94,195,196],{},"\u002F\u002F Native ecosystem syntax works too\n",[94,198,199],{"class":96,"line":122},[94,200,201],{},"r, _ = vers.ParseNative(\"~> 1.2.3\", \"gem\")\n",[94,203,204],{"class":96,"line":127},[94,205,206],{},"r.Contains(\"1.2.5\")  \u002F\u002F true\n",[94,208,209],{"class":96,"line":133},[94,210,211],{},"r.Contains(\"1.3.0\")  \u002F\u002F false\n",[62,213,215],{"id":214},"spdx",[21,216,214],{"href":217,"rel":218},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fspdx",[25],[17,220,221],{},"Package registries are full of informal license strings like \"Apache 2\", \"MIT License\", \"GPL v3\" that need normalizing into valid SPDX identifiers before you can do anything useful with them. This handles that, along with parsing compound expressions with AND\u002FOR operators, checking license compatibility, and categorizing licenses using the scancode-licensedb database (updated weekly).",[84,223,225],{"className":86,"code":224,"language":88,"meta":89,"style":89},"id, _ := spdx.Normalize(\"Apache 2\")  \u002F\u002F \"Apache-2.0\"\n\nexpr, _ := spdx.Parse(\"Apache 2 OR MIT License\")\nexpr.String()  \u002F\u002F \"Apache-2.0 OR MIT\"\n\nspdx.Satisfies(\"MIT OR Apache-2.0\", []string{\"MIT\"})  \u002F\u002F true\nspdx.IsPermissive(\"MIT\")                               \u002F\u002F true\nspdx.HasCopyleft(\"MIT OR GPL-3.0-only\")                \u002F\u002F true\n",[91,226,227,232,236,241,246,250,255,260],{"__ignoreMap":89},[94,228,229],{"class":96,"line":97},[94,230,231],{},"id, _ := spdx.Normalize(\"Apache 2\")  \u002F\u002F \"Apache-2.0\"\n",[94,233,234],{"class":96,"line":103},[94,235,113],{"emptyLinePlaceholder":112},[94,237,238],{"class":96,"line":109},[94,239,240],{},"expr, _ := spdx.Parse(\"Apache 2 OR MIT License\")\n",[94,242,243],{"class":96,"line":116},[94,244,245],{},"expr.String()  \u002F\u002F \"Apache-2.0 OR MIT\"\n",[94,247,248],{"class":96,"line":122},[94,249,113],{"emptyLinePlaceholder":112},[94,251,252],{"class":96,"line":127},[94,253,254],{},"spdx.Satisfies(\"MIT OR Apache-2.0\", []string{\"MIT\"})  \u002F\u002F true\n",[94,256,257],{"class":96,"line":133},[94,258,259],{},"spdx.IsPermissive(\"MIT\")                               \u002F\u002F true\n",[94,261,262],{"class":96,"line":139},[94,263,264],{},"spdx.HasCopyleft(\"MIT OR GPL-3.0-only\")                \u002F\u002F true\n",[62,266,268],{"id":267},"platforms",[21,269,267],{"href":270,"rel":271},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fplatforms",[25],[17,273,274,275,279,280,283,284,287,288,291,292,295],{},"I wrote about ",[21,276,278],{"href":277},"\u002Freports\u002Fplatform-strings","platform string fragmentation"," recently: Go uses ",[91,281,282],{},"darwin\u002Farm64",", Node uses ",[91,285,286],{},"darwin-arm64",", Rust uses ",[91,289,290],{},"aarch64-apple-darwin",", RubyGems uses ",[91,293,294],{},"arm64-darwin",", all for the same chip on the same OS. This translates between 14 ecosystems through a canonical intermediate representation:",[84,297,299],{"className":86,"code":298,"language":88,"meta":89,"style":89},"p, _ := platforms.Parse(platforms.Go, \"darwin\u002Farm64\")\n\u002F\u002F p.Arch == \"aarch64\", p.OS == \"darwin\"\n\ns, _ := platforms.Format(platforms.Rust, p)\n\u002F\u002F \"aarch64-apple-darwin\"\n\n\u002F\u002F Or translate directly\ns, _ = platforms.Translate(platforms.Go, platforms.RubyGems, \"darwin\u002Farm64\")\n\u002F\u002F \"arm64-darwin\"\n",[91,300,301,306,311,315,320,325,329,334,339],{"__ignoreMap":89},[94,302,303],{"class":96,"line":97},[94,304,305],{},"p, _ := platforms.Parse(platforms.Go, \"darwin\u002Farm64\")\n",[94,307,308],{"class":96,"line":103},[94,309,310],{},"\u002F\u002F p.Arch == \"aarch64\", p.OS == \"darwin\"\n",[94,312,313],{"class":96,"line":109},[94,314,113],{"emptyLinePlaceholder":112},[94,316,317],{"class":96,"line":116},[94,318,319],{},"s, _ := platforms.Format(platforms.Rust, p)\n",[94,321,322],{"class":96,"line":122},[94,323,324],{},"\u002F\u002F \"aarch64-apple-darwin\"\n",[94,326,327],{"class":96,"line":127},[94,328,113],{"emptyLinePlaceholder":112},[94,330,331],{"class":96,"line":133},[94,332,333],{},"\u002F\u002F Or translate directly\n",[94,335,336],{"class":96,"line":139},[94,337,338],{},"s, _ = platforms.Translate(platforms.Go, platforms.RubyGems, \"darwin\u002Farm64\")\n",[94,340,342],{"class":96,"line":341},9,[94,343,344],{},"\u002F\u002F \"arm64-darwin\"\n",[57,346,348],{"id":347},"data-sources","Data sources",[62,350,352],{"id":351},"registries",[21,353,351],{"href":354,"rel":355},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fregistries",[25],[17,357,358,359,364],{},"Talks to 25 package registry APIs (npm, PyPI, Cargo, RubyGems, Maven, NuGet, Hex, Pub, CocoaPods, Homebrew, and more) and returns normalized package information including versions, dependencies, maintainers, and licenses. Works a lot like the internals of ",[21,360,363],{"href":361,"rel":362},"https:\u002F\u002Fpackages.ecosyste.ms",[25],"packages.ecosyste.ms",", taking PURLs as input so you don't need to know the quirks of each registry's API.",[84,366,368],{"className":86,"code":367,"language":88,"meta":89,"style":89},"import (\n    \"github.com\u002Fgit-pkgs\u002Fregistries\"\n    _ \"github.com\u002Fgit-pkgs\u002Fregistries\u002Fall\"\n)\n\npkg, _ := registries.FetchPackageFromPURL(ctx, \"pkg:cargo\u002Fserde\", nil)\nfmt.Println(pkg.Repository)  \u002F\u002F \"https:\u002F\u002Fgithub.com\u002Fserde-rs\u002Fserde\"\nfmt.Println(pkg.Licenses)    \u002F\u002F \"MIT OR Apache-2.0\"\n\n\u002F\u002F Bulk fetch with parallel requests\npackages := registries.BulkFetchPackages(ctx, []string{\n    \"pkg:npm\u002Flodash@4.17.21\",\n    \"pkg:cargo\u002Fserde@1.0.0\",\n    \"pkg:pypi\u002Frequests@2.31.0\",\n}, nil)\n",[91,369,370,375,380,385,390,394,399,404,409,413,419,425,431,437,443],{"__ignoreMap":89},[94,371,372],{"class":96,"line":97},[94,373,374],{},"import (\n",[94,376,377],{"class":96,"line":103},[94,378,379],{},"    \"github.com\u002Fgit-pkgs\u002Fregistries\"\n",[94,381,382],{"class":96,"line":109},[94,383,384],{},"    _ \"github.com\u002Fgit-pkgs\u002Fregistries\u002Fall\"\n",[94,386,387],{"class":96,"line":116},[94,388,389],{},")\n",[94,391,392],{"class":96,"line":122},[94,393,113],{"emptyLinePlaceholder":112},[94,395,396],{"class":96,"line":127},[94,397,398],{},"pkg, _ := registries.FetchPackageFromPURL(ctx, \"pkg:cargo\u002Fserde\", nil)\n",[94,400,401],{"class":96,"line":133},[94,402,403],{},"fmt.Println(pkg.Repository)  \u002F\u002F \"https:\u002F\u002Fgithub.com\u002Fserde-rs\u002Fserde\"\n",[94,405,406],{"class":96,"line":139},[94,407,408],{},"fmt.Println(pkg.Licenses)    \u002F\u002F \"MIT OR Apache-2.0\"\n",[94,410,411],{"class":96,"line":341},[94,412,113],{"emptyLinePlaceholder":112},[94,414,416],{"class":96,"line":415},10,[94,417,418],{},"\u002F\u002F Bulk fetch with parallel requests\n",[94,420,422],{"class":96,"line":421},11,[94,423,424],{},"packages := registries.BulkFetchPackages(ctx, []string{\n",[94,426,428],{"class":96,"line":427},12,[94,429,430],{},"    \"pkg:npm\u002Flodash@4.17.21\",\n",[94,432,434],{"class":96,"line":433},13,[94,435,436],{},"    \"pkg:cargo\u002Fserde@1.0.0\",\n",[94,438,440],{"class":96,"line":439},14,[94,441,442],{},"    \"pkg:pypi\u002Frequests@2.31.0\",\n",[94,444,446],{"class":96,"line":445},15,[94,447,448],{},"}, nil)\n",[17,450,451],{},"Private registries work through PURL qualifiers, and rate-limited APIs get automatic retries with exponential backoff.",[62,453,455],{"id":454},"forges",[21,456,454],{"href":457,"rel":458},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fforges",[25],[17,460,461,462,467],{},"Fetches repository metadata from GitHub, GitLab, Gitea, Forgejo, and Bitbucket, normalizing it into a common structure similar to how ",[21,463,466],{"href":464,"rel":465},"https:\u002F\u002Frepos.ecosyste.ms",[25],"repos.ecosyste.ms"," works under the hood. Point it at a self-hosted domain and it'll probe the API to figure out which forge software is running:",[84,469,471],{"className":86,"code":470,"language":88,"meta":89,"style":89},"client := forges.NewClient(\n    forges.WithToken(\"github.com\", os.Getenv(\"GITHUB_TOKEN\")),\n)\n\nrepo, _ := client.FetchRepository(ctx, \"https:\u002F\u002Fgithub.com\u002Foctocat\u002Fhello-world\")\nrepo.License          \u002F\u002F \"MIT\"\nrepo.StargazersCount  \u002F\u002F 12345\n\n\u002F\u002F Auto-detect forge type for self-hosted instances\nclient.RegisterDomain(ctx, \"git.example.com\", token)\n",[91,472,473,478,483,487,491,496,501,506,510,515],{"__ignoreMap":89},[94,474,475],{"class":96,"line":97},[94,476,477],{},"client := forges.NewClient(\n",[94,479,480],{"class":96,"line":103},[94,481,482],{},"    forges.WithToken(\"github.com\", os.Getenv(\"GITHUB_TOKEN\")),\n",[94,484,485],{"class":96,"line":109},[94,486,389],{},[94,488,489],{"class":96,"line":116},[94,490,113],{"emptyLinePlaceholder":112},[94,492,493],{"class":96,"line":122},[94,494,495],{},"repo, _ := client.FetchRepository(ctx, \"https:\u002F\u002Fgithub.com\u002Foctocat\u002Fhello-world\")\n",[94,497,498],{"class":96,"line":127},[94,499,500],{},"repo.License          \u002F\u002F \"MIT\"\n",[94,502,503],{"class":96,"line":133},[94,504,505],{},"repo.StargazersCount  \u002F\u002F 12345\n",[94,507,508],{"class":96,"line":139},[94,509,113],{"emptyLinePlaceholder":112},[94,511,512],{"class":96,"line":341},[94,513,514],{},"\u002F\u002F Auto-detect forge type for self-hosted instances\n",[94,516,517],{"class":96,"line":415},[94,518,519],{},"client.RegisterDomain(ctx, \"git.example.com\", token)\n",[62,521,523],{"id":522},"enrichment",[21,524,522],{"href":525,"rel":526},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fenrichment",[25],[17,528,529,530,532,533,535,536,540,541,540,546,551,552,554,555,558],{},"Where ",[91,531,351],{}," talks to one registry at a time, ",[91,534,522],{}," routes requests across four data sources: ",[21,537,539],{"href":39,"rel":538},[25],"ecosyste.ms",", ",[21,542,545],{"href":543,"rel":544},"https:\u002F\u002Fdeps.dev",[25],"deps.dev",[21,547,550],{"href":548,"rel":549},"https:\u002F\u002Fsecurityscorecards.dev",[25],"OpenSSF Scorecard",", and direct registry queries via the ",[91,553,351],{}," module. PURLs with a ",[91,556,557],{},"repository_url"," qualifier go directly to custom registries, others go through ecosyste.ms or deps.dev, and each result records which source it came from.",[84,560,562],{"className":86,"code":561,"language":88,"meta":89,"style":89},"client, _ := enrichment.NewClient()\n\nresults, _ := client.BulkLookup(ctx, []string{\n    \"pkg:npm\u002Flodash\",\n    \"pkg:pypi\u002Frequests\",\n})\n\ninfo := results[\"pkg:npm\u002Flodash\"]\nfmt.Println(info.LatestVersion)  \u002F\u002F \"4.17.21\"\nfmt.Println(info.License)        \u002F\u002F \"MIT\"\nfmt.Println(info.Source)         \u002F\u002F \"ecosystems\", \"registries\", or \"depsdev\"\n\n\u002F\u002F Scorecard is a separate client for repo-level security scores\nsc := scorecard.New()\nresult, _ := sc.GetScore(ctx, \"github.com\u002Flodash\u002Flodash\")\nfmt.Println(result.Score)  \u002F\u002F 6.8\n",[91,563,564,569,573,578,583,588,593,597,602,607,612,617,621,626,631,636],{"__ignoreMap":89},[94,565,566],{"class":96,"line":97},[94,567,568],{},"client, _ := enrichment.NewClient()\n",[94,570,571],{"class":96,"line":103},[94,572,113],{"emptyLinePlaceholder":112},[94,574,575],{"class":96,"line":109},[94,576,577],{},"results, _ := client.BulkLookup(ctx, []string{\n",[94,579,580],{"class":96,"line":116},[94,581,582],{},"    \"pkg:npm\u002Flodash\",\n",[94,584,585],{"class":96,"line":122},[94,586,587],{},"    \"pkg:pypi\u002Frequests\",\n",[94,589,590],{"class":96,"line":127},[94,591,592],{},"})\n",[94,594,595],{"class":96,"line":133},[94,596,113],{"emptyLinePlaceholder":112},[94,598,599],{"class":96,"line":139},[94,600,601],{},"info := results[\"pkg:npm\u002Flodash\"]\n",[94,603,604],{"class":96,"line":341},[94,605,606],{},"fmt.Println(info.LatestVersion)  \u002F\u002F \"4.17.21\"\n",[94,608,609],{"class":96,"line":415},[94,610,611],{},"fmt.Println(info.License)        \u002F\u002F \"MIT\"\n",[94,613,614],{"class":96,"line":421},[94,615,616],{},"fmt.Println(info.Source)         \u002F\u002F \"ecosystems\", \"registries\", or \"depsdev\"\n",[94,618,619],{"class":96,"line":427},[94,620,113],{"emptyLinePlaceholder":112},[94,622,623],{"class":96,"line":433},[94,624,625],{},"\u002F\u002F Scorecard is a separate client for repo-level security scores\n",[94,627,628],{"class":96,"line":439},[94,629,630],{},"sc := scorecard.New()\n",[94,632,633],{"class":96,"line":445},[94,634,635],{},"result, _ := sc.GetScore(ctx, \"github.com\u002Flodash\u002Flodash\")\n",[94,637,639],{"class":96,"line":638},16,[94,640,641],{},"fmt.Println(result.Score)  \u002F\u002F 6.8\n",[62,643,645],{"id":644},"vulns",[21,646,644],{"href":647,"rel":648},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fvulns",[25],[17,650,651,652,540,657,540,660,540,665,540,670,540,675,680,681,686],{},"Seven vulnerability data sources behind one interface: ",[21,653,656],{"href":654,"rel":655},"https:\u002F\u002Fosv.dev",[25],"OSV",[21,658,545],{"href":543,"rel":659},[25],[21,661,664],{"href":662,"rel":663},"https:\u002F\u002Fgithub.com\u002Fadvisories",[25],"GitHub Security Advisories",[21,666,669],{"href":667,"rel":668},"https:\u002F\u002Fnvd.nist.gov",[25],"NVD",[21,671,674],{"href":672,"rel":673},"https:\u002F\u002Fgithub.com\u002Fanchore\u002Fgrype",[25],"Grype",[21,676,679],{"href":677,"rel":678},"https:\u002F\u002Fvulncheck.com",[25],"VulnCheck",", and ",[21,682,685],{"href":683,"rel":684},"https:\u002F\u002Fvulnerability.circl.lu",[25],"Vulnerability-Lookup",". Results are normalized to OSV format with built-in CVSS parsing for v2.0 through v4.0:",[84,688,690],{"className":86,"code":689,"language":88,"meta":89,"style":89},"source := osv.New()\n\nresults, _ := source.Query(ctx, purl.MakePURL(\"npm\", \"lodash\", \"4.17.20\"))\nfor _, v := range results {\n    fmt.Printf(\"%s: %s (severity: %s)\\n\", v.ID, v.Summary, v.SeverityLevel())\n    if fixed := v.FixedVersion(\"npm\", \"lodash\"); fixed != \"\" {\n        fmt.Printf(\"  Fixed in: %s\\n\", fixed)\n    }\n}\n",[91,691,692,697,701,706,711,716,721,726,731],{"__ignoreMap":89},[94,693,694],{"class":96,"line":97},[94,695,696],{},"source := osv.New()\n",[94,698,699],{"class":96,"line":103},[94,700,113],{"emptyLinePlaceholder":112},[94,702,703],{"class":96,"line":109},[94,704,705],{},"results, _ := source.Query(ctx, purl.MakePURL(\"npm\", \"lodash\", \"4.17.20\"))\n",[94,707,708],{"class":96,"line":116},[94,709,710],{},"for _, v := range results {\n",[94,712,713],{"class":96,"line":122},[94,714,715],{},"    fmt.Printf(\"%s: %s (severity: %s)\\n\", v.ID, v.Summary, v.SeverityLevel())\n",[94,717,718],{"class":96,"line":127},[94,719,720],{},"    if fixed := v.FixedVersion(\"npm\", \"lodash\"); fixed != \"\" {\n",[94,722,723],{"class":96,"line":133},[94,724,725],{},"        fmt.Printf(\"  Fixed in: %s\\n\", fixed)\n",[94,727,728],{"class":96,"line":139},[94,729,730],{},"    }\n",[94,732,733],{"class":96,"line":341},[94,734,735],{},"}\n",[17,737,738],{},"All sources support batch queries, with limits ranging from 1,000 to 5,000 packages per request depending on the source.",[57,740,742],{"id":741},"file-handling","File handling",[62,744,746],{"id":745},"manifests",[21,747,745],{"href":748,"rel":749},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fmanifests",[25],[17,751,752],{},"Parses manifest and lockfiles across 40+ ecosystems, auto-detecting file types and extracting dependencies with version constraints, scopes, integrity hashes, and PURLs. It distinguishes between manifests (declared dependencies), lockfiles (resolved versions), and supplements (extra metadata).",[84,754,756],{"className":86,"code":755,"language":88,"meta":89,"style":89},"content, _ := os.ReadFile(\"package.json\")\nresult, _ := manifests.Parse(\"package.json\", content)\n\nfmt.Println(result.Ecosystem)  \u002F\u002F \"npm\"\nfmt.Println(result.Kind)       \u002F\u002F \"manifest\"\nfor _, dep := range result.Dependencies {\n    fmt.Printf(\"%s@%s (%s)\\n\", dep.Name, dep.Version, dep.Scope)\n}\n",[91,757,758,763,768,772,777,782,787,792],{"__ignoreMap":89},[94,759,760],{"class":96,"line":97},[94,761,762],{},"content, _ := os.ReadFile(\"package.json\")\n",[94,764,765],{"class":96,"line":103},[94,766,767],{},"result, _ := manifests.Parse(\"package.json\", content)\n",[94,769,770],{"class":96,"line":109},[94,771,113],{"emptyLinePlaceholder":112},[94,773,774],{"class":96,"line":116},[94,775,776],{},"fmt.Println(result.Ecosystem)  \u002F\u002F \"npm\"\n",[94,778,779],{"class":96,"line":122},[94,780,781],{},"fmt.Println(result.Kind)       \u002F\u002F \"manifest\"\n",[94,783,784],{"class":96,"line":127},[94,785,786],{},"for _, dep := range result.Dependencies {\n",[94,788,789],{"class":96,"line":133},[94,790,791],{},"    fmt.Printf(\"%s@%s (%s)\\n\", dep.Name, dep.Version, dep.Scope)\n",[94,793,794],{"class":96,"line":139},[94,795,735],{},[17,797,798],{},"Supported formats range from the obvious (package.json, Gemfile.lock, go.mod) to the less common (APKBUILD, PKGBUILD, .rockspec, dub.sdl). Each dependency includes its name, version constraint, scope (runtime, development, test, build, optional), integrity hash when available, and whether it's a direct or transitive dependency.",[62,800,802],{"id":801},"resolve",[21,803,801],{"href":804,"rel":805},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fresolve",[25],[17,807,529,808,810,811,813,814,540,817,540,820,823],{},[91,809,745],{}," parses static files, ",[91,812,801],{}," parses the runtime output of package manager CLI commands (",[91,815,816],{},"npm ls --json",[91,818,819],{},"go mod graph",[91,821,822],{},"uv tree",", etc.) into a normalized dependency graph with PURLs. It supports 24+ managers and preserves tree structure when the manager provides it:",[84,825,827],{"className":86,"code":826,"language":88,"meta":89,"style":89},"import (\n    \"github.com\u002Fgit-pkgs\u002Fresolve\"\n    _ \"github.com\u002Fgit-pkgs\u002Fresolve\u002Fparsers\"\n)\n\noutput, _ := exec.Command(\"npm\", \"ls\", \"--json\", \"--long\").Output()\nresult, _ := resolve.Parse(\"npm\", output)\n\nfor _, dep := range result.Direct {\n    fmt.Printf(\"%s@%s (%s)\\n\", dep.Name, dep.Version, dep.PURL)\n    for _, transitive := range dep.Deps {\n        fmt.Printf(\"  %s@%s\\n\", transitive.Name, transitive.Version)\n    }\n}\n",[91,828,829,833,838,843,847,851,856,861,865,870,875,880,885,889],{"__ignoreMap":89},[94,830,831],{"class":96,"line":97},[94,832,374],{},[94,834,835],{"class":96,"line":103},[94,836,837],{},"    \"github.com\u002Fgit-pkgs\u002Fresolve\"\n",[94,839,840],{"class":96,"line":109},[94,841,842],{},"    _ \"github.com\u002Fgit-pkgs\u002Fresolve\u002Fparsers\"\n",[94,844,845],{"class":96,"line":116},[94,846,389],{},[94,848,849],{"class":96,"line":122},[94,850,113],{"emptyLinePlaceholder":112},[94,852,853],{"class":96,"line":127},[94,854,855],{},"output, _ := exec.Command(\"npm\", \"ls\", \"--json\", \"--long\").Output()\n",[94,857,858],{"class":96,"line":133},[94,859,860],{},"result, _ := resolve.Parse(\"npm\", output)\n",[94,862,863],{"class":96,"line":139},[94,864,113],{"emptyLinePlaceholder":112},[94,866,867],{"class":96,"line":341},[94,868,869],{},"for _, dep := range result.Direct {\n",[94,871,872],{"class":96,"line":415},[94,873,874],{},"    fmt.Printf(\"%s@%s (%s)\\n\", dep.Name, dep.Version, dep.PURL)\n",[94,876,877],{"class":96,"line":421},[94,878,879],{},"    for _, transitive := range dep.Deps {\n",[94,881,882],{"class":96,"line":427},[94,883,884],{},"        fmt.Printf(\"  %s@%s\\n\", transitive.Name, transitive.Version)\n",[94,886,887],{"class":96,"line":433},[94,888,730],{},[94,890,891],{"class":96,"line":439},[94,892,735],{},[62,894,896],{"id":895},"archives",[21,897,895],{"href":898,"rel":899},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Farchives",[25],[17,901,902,903,906,907,911],{},"Reads and browses archive files entirely in memory, with a unified Reader interface across ZIP, tar (with gzip, bzip2, xz compression), jar, wheel, nupkg, egg, and Ruby gems. Includes prefix stripping for packages that wrap content in a directory (like npm's ",[91,904,905],{},"package\u002F"," wrapper). No ",[21,908,910],{"href":909},"\u002Fideas\u002Fwhat-package-registries-could-borrow-from-oci","OCI support"," yet, but pulling and browsing image layers through the same Reader interface is on the list.",[84,913,915],{"className":86,"code":914,"language":88,"meta":89,"style":89},"reader, _ := archives.Open(\"package.tar.gz\", f)\ndefer reader.Close()\n\nfiles, _ := reader.List()\nfor _, fi := range files {\n    fmt.Println(fi.Path, fi.Size)\n}\n\nrc, _ := reader.Extract(\"README.md\")\ndefer rc.Close()\n",[91,916,917,922,927,931,936,941,946,950,954,959],{"__ignoreMap":89},[94,918,919],{"class":96,"line":97},[94,920,921],{},"reader, _ := archives.Open(\"package.tar.gz\", f)\n",[94,923,924],{"class":96,"line":103},[94,925,926],{},"defer reader.Close()\n",[94,928,929],{"class":96,"line":109},[94,930,113],{"emptyLinePlaceholder":112},[94,932,933],{"class":96,"line":116},[94,934,935],{},"files, _ := reader.List()\n",[94,937,938],{"class":96,"line":122},[94,939,940],{},"for _, fi := range files {\n",[94,942,943],{"class":96,"line":127},[94,944,945],{},"    fmt.Println(fi.Path, fi.Size)\n",[94,947,948],{"class":96,"line":133},[94,949,735],{},[94,951,952],{"class":96,"line":139},[94,953,113],{"emptyLinePlaceholder":112},[94,955,956],{"class":96,"line":341},[94,957,958],{},"rc, _ := reader.Extract(\"README.md\")\n",[94,960,961],{"class":96,"line":415},[94,962,963],{},"defer rc.Close()\n",[62,965,967],{"id":966},"changelog",[21,968,966],{"href":969,"rel":970},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fchangelog",[25],[17,972,973,974,979],{},"Parses changelog files into structured entries, auto-detecting ",[21,975,978],{"href":976,"rel":977},"https:\u002F\u002Fkeepachangelog.com",[25],"Keep a Changelog",", markdown header, and setext\u002Funderline formats. You can supply custom regex patterns for non-standard formats, and there's a finder that searches for common changelog filenames in a directory:",[84,981,983],{"className":86,"code":982,"language":88,"meta":89,"style":89},"p, _ := changelog.FindAndParse(\".\")\n\nfor _, v := range p.Versions() {\n    entry, _ := p.Entry(v)\n    fmt.Printf(\"%s (%v): %s\\n\", v, entry.Date, entry.Content)\n}\n\n\u002F\u002F Content between two versions, like Dependabot uses\ncontent, _ := p.Between(\"1.0.0\", \"2.0.0\")\n",[91,984,985,990,994,999,1004,1009,1013,1017,1022],{"__ignoreMap":89},[94,986,987],{"class":96,"line":97},[94,988,989],{},"p, _ := changelog.FindAndParse(\".\")\n",[94,991,992],{"class":96,"line":103},[94,993,113],{"emptyLinePlaceholder":112},[94,995,996],{"class":96,"line":109},[94,997,998],{},"for _, v := range p.Versions() {\n",[94,1000,1001],{"class":96,"line":116},[94,1002,1003],{},"    entry, _ := p.Entry(v)\n",[94,1005,1006],{"class":96,"line":122},[94,1007,1008],{},"    fmt.Printf(\"%s (%v): %s\\n\", v, entry.Date, entry.Content)\n",[94,1010,1011],{"class":96,"line":127},[94,1012,735],{},[94,1014,1015],{"class":96,"line":133},[94,1016,113],{"emptyLinePlaceholder":112},[94,1018,1019],{"class":96,"line":139},[94,1020,1021],{},"\u002F\u002F Content between two versions, like Dependabot uses\n",[94,1023,1024],{"class":96,"line":341},[94,1025,1026],{},"content, _ := p.Between(\"1.0.0\", \"2.0.0\")\n",[62,1028,1030],{"id":1029},"gitignore",[21,1031,1029],{"href":1032,"rel":1033},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fgitignore",[25],[17,1035,1036,1037,1041,1042,1045],{},"Matches paths against ",[21,1038,1040],{"href":1039},"\u002Freports\u002Fthe-many-flavors-of-ignore-files","gitignore rules"," using a direct implementation of git's wildmatch algorithm rather than converting patterns to regexes, tested against git's own wildmatch test suite. Handles nested ",[91,1043,1044],{},".gitignore"," files scoped to their directories, global excludes, negation patterns, and all 12 POSIX character classes:",[84,1047,1049],{"className":86,"code":1048,"language":88,"meta":89,"style":89},"m := gitignore.NewFromDirectory(\"\u002Fpath\u002Fto\u002Frepo\")\n\nm.Match(\"vendor\u002Flib.go\")  \u002F\u002F true if matched\n\nr := m.MatchDetail(\"app.log\")\nif r.Matched {\n    fmt.Printf(\"ignored by %s (line %d of %s)\\n\", r.Pattern, r.Line, r.Source)\n}\n\n\u002F\u002F Walk a directory, skipping ignored entries\ngitignore.Walk(\"\u002Fpath\u002Fto\u002Frepo\", func(path string, d fs.DirEntry) error {\n    fmt.Println(path)\n    return nil\n})\n",[91,1050,1051,1056,1060,1065,1069,1074,1079,1084,1088,1092,1097,1102,1107,1112],{"__ignoreMap":89},[94,1052,1053],{"class":96,"line":97},[94,1054,1055],{},"m := gitignore.NewFromDirectory(\"\u002Fpath\u002Fto\u002Frepo\")\n",[94,1057,1058],{"class":96,"line":103},[94,1059,113],{"emptyLinePlaceholder":112},[94,1061,1062],{"class":96,"line":109},[94,1063,1064],{},"m.Match(\"vendor\u002Flib.go\")  \u002F\u002F true if matched\n",[94,1066,1067],{"class":96,"line":116},[94,1068,113],{"emptyLinePlaceholder":112},[94,1070,1071],{"class":96,"line":122},[94,1072,1073],{},"r := m.MatchDetail(\"app.log\")\n",[94,1075,1076],{"class":96,"line":127},[94,1077,1078],{},"if r.Matched {\n",[94,1080,1081],{"class":96,"line":133},[94,1082,1083],{},"    fmt.Printf(\"ignored by %s (line %d of %s)\\n\", r.Pattern, r.Line, r.Source)\n",[94,1085,1086],{"class":96,"line":139},[94,1087,735],{},[94,1089,1090],{"class":96,"line":341},[94,1091,113],{"emptyLinePlaceholder":112},[94,1093,1094],{"class":96,"line":415},[94,1095,1096],{},"\u002F\u002F Walk a directory, skipping ignored entries\n",[94,1098,1099],{"class":96,"line":421},[94,1100,1101],{},"gitignore.Walk(\"\u002Fpath\u002Fto\u002Frepo\", func(path string, d fs.DirEntry) error {\n",[94,1103,1104],{"class":96,"line":427},[94,1105,1106],{},"    fmt.Println(path)\n",[94,1108,1109],{"class":96,"line":433},[94,1110,1111],{},"    return nil\n",[94,1113,1114],{"class":96,"line":439},[94,1115,592],{},[57,1117,1119],{"id":1118},"tooling","Tooling",[62,1121,1123],{"id":1122},"managers",[21,1124,1122],{"href":1125,"rel":1126},"https:\u002F\u002Fgithub.com\u002Fgit-pkgs\u002Fmanagers",[25],[17,1128,1129],{},"Wraps 34 package manager CLIs behind a common interface where you describe what you want (add a dependency, list installed packages, update) and get the correct CLI invocation back. Package managers are defined in YAML files, so adding a new one doesn't require code changes:",[84,1131,1133],{"className":86,"code":1132,"language":88,"meta":89,"style":89},"translator := managers.NewTranslator()\n\ncmd, _ := translator.BuildCommand(\"npm\", \"add\", managers.CommandInput{\n    Args:  map[string]string{\"package\": \"lodash\"},\n    Flags: map[string]any{\"dev\": true},\n})\n\u002F\u002F [\"npm\", \"install\", \"lodash\", \"--save-dev\"]\n\ncmd, _ = translator.BuildCommand(\"bundler\", \"add\", managers.CommandInput{\n    Args:  map[string]string{\"package\": \"rails\"},\n    Flags: map[string]any{\"dev\": true},\n})\n\u002F\u002F [\"bundle\", \"add\", \"rails\", \"--group\", \"development\"]\n",[91,1134,1135,1140,1144,1149,1154,1159,1163,1168,1172,1177,1182,1186,1190],{"__ignoreMap":89},[94,1136,1137],{"class":96,"line":97},[94,1138,1139],{},"translator := managers.NewTranslator()\n",[94,1141,1142],{"class":96,"line":103},[94,1143,113],{"emptyLinePlaceholder":112},[94,1145,1146],{"class":96,"line":109},[94,1147,1148],{},"cmd, _ := translator.BuildCommand(\"npm\", \"add\", managers.CommandInput{\n",[94,1150,1151],{"class":96,"line":116},[94,1152,1153],{},"    Args:  map[string]string{\"package\": \"lodash\"},\n",[94,1155,1156],{"class":96,"line":122},[94,1157,1158],{},"    Flags: map[string]any{\"dev\": true},\n",[94,1160,1161],{"class":96,"line":127},[94,1162,592],{},[94,1164,1165],{"class":96,"line":133},[94,1166,1167],{},"\u002F\u002F [\"npm\", \"install\", \"lodash\", \"--save-dev\"]\n",[94,1169,1170],{"class":96,"line":139},[94,1171,113],{"emptyLinePlaceholder":112},[94,1173,1174],{"class":96,"line":341},[94,1175,1176],{},"cmd, _ = translator.BuildCommand(\"bundler\", \"add\", managers.CommandInput{\n",[94,1178,1179],{"class":96,"line":415},[94,1180,1181],{},"    Args:  map[string]string{\"package\": \"rails\"},\n",[94,1183,1184],{"class":96,"line":421},[94,1185,1158],{},[94,1187,1188],{"class":96,"line":427},[94,1189,592],{},[94,1191,1192],{"class":96,"line":433},[94,1193,1194],{},"\u002F\u002F [\"bundle\", \"add\", \"rails\", \"--group\", \"development\"]\n",[17,1196,1197,1198,1203,1204,1208],{},"The command definitions started as data from the ",[21,1199,1202],{"href":1200,"rel":1201},"https:\u002F\u002Fgithub.com\u002Fecosyste-ms\u002Fpackage-manager-commands",[25],"package manager command crosswalk"," I built for Ecosyste.ms. Because it can drive any package manager agnostically, it opens up some interesting possibilities: setting up GitHub Actions workflows that work regardless of ecosystem, installing dependencies in git hooks without hardcoding the manager, or building tools like Dependabot that operate across all 34 managers with the same code. There's an ",[21,1205,1207],{"href":1125,"rel":1206},[25],"example Dependabot-style workflow"," in the repo.",[17,1210,1211,1212,1215],{},"It can auto-detect which manager is in use from lockfiles or manifests, and has a pluggable policy system that runs checks before commands execute: a ",[91,1213,1214],{},"PackageBlocklistPolicy"," prevents installing known-bad packages, and you can write your own to enforce license compliance, restrict registries, or gate operations behind approval.",[1217,1218],"hr",{},[17,1220,1221,1222,1224,1225,1227,1228,1230,1231,1233],{},"PURLs act as the common identifier across all of these, which is what makes them composable. You might parse a lockfile with ",[91,1223,745],{}," to get a list of dependencies as PURLs, enrich them with ",[91,1226,351],{}," to pull in license and repository metadata, check them against ",[91,1229,644],{}," for known vulnerabilities, and normalize their license strings with ",[91,1232,214],{}," for compliance reporting. Four modules, no translation layer between them.",[17,1235,1236,1237,1241],{},"All the modules are MIT licensed and available under the ",[21,1238,1240],{"href":23,"rel":1239},[25],"git-pkgs org",".",[1243,1244,1245],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":89,"searchDepth":103,"depth":103,"links":1247},[1248,1254,1260,1267],{"id":59,"depth":103,"text":60,"children":1249},[1250,1251,1252,1253],{"id":64,"depth":109,"text":64},{"id":145,"depth":109,"text":145},{"id":214,"depth":109,"text":214},{"id":267,"depth":109,"text":267},{"id":347,"depth":103,"text":348,"children":1255},[1256,1257,1258,1259],{"id":351,"depth":109,"text":351},{"id":454,"depth":109,"text":454},{"id":522,"depth":109,"text":522},{"id":644,"depth":109,"text":644},{"id":741,"depth":103,"text":742,"children":1261},[1262,1263,1264,1265,1266],{"id":745,"depth":109,"text":745},{"id":801,"depth":109,"text":801},{"id":895,"depth":109,"text":895},{"id":966,"depth":109,"text":966},{"id":1029,"depth":109,"text":1029},{"id":1118,"depth":103,"text":1119,"children":1268},[1269],{"id":1122,"depth":109,"text":1122},"https:\u002F\u002Fnesbitt.io\u002F2026\u002F02\u002F19\u002Fgo-modules-for-package-management-tooling","nesbitt.io","2026-02-19","The Go modules behind git-pkgs, rebuilt from my Ruby supply chain libraries.","md",false,null,{},"\u002Ftools\u002Fgo-modules-for-package-management-tooling",{"title":10,"description":1273},"tools\u002Fgo-modules-for-package-management-tooling","Rp1qdjbdqbsimRw0MnaJ4Rc51mFi4mxKjOMEDfHBPKs",1780596103602]