8. cmd/update.go - parse PURL when package arg is provided:
Updated task #2 status
Now let me verify the build compiles before writing tests.
cd /Users/andrew/code/git-pkgs/git-pkgs && go build ./...(empty result)
Clean build. Now let me write the tests.
Updated task #3 status
Updated task #3 status
Updated task #4 status
Now let me run the new tests first.
go test ./cmd/ -run TestParsePackageArg -v 2>&1=== RUN TestParsePackageArg
=== RUN TestParsePackageArg/plain_name_passes_through_ecosystem_flag
=== RUN TestParsePackageArg/plain_name_with_empty_ecosystem_flag
=== RUN TestParsePackageArg/PURL_extracts_ecosystem_name_and_version
=== RUN TestParsePackageArg/PURL_without_version
=== RUN TestParsePackageArg/PURL_with_namespace
=== RUN TestParsePackageArg/PURL_ignores_ecosystem_flag
=== RUN TestParsePackageArg/invalid_PURL_returns_error
=== RUN TestParsePackageArg/gem_PURL_maps_to_rubygems_ecosystem
=== RUN TestParsePackageArg/golang_PURL_with_namespace
--- PASS: TestParsePackageArg (0.00s)
--- PASS: TestParsePackageArg/plain_name_passes_through_ecosystem_flag (0.00s)
--- PASS: TestParsePackageArg/plain_name_with_empty_ecosystem_flag (0.00s)
--- PASS: TestParsePackageArg/PURL_extracts_ecosystem_name_and_version (0.00s)
--- PASS: TestParsePackageArg/PURL_without_version (0.00s)
--- PASS: TestParsePackageArg/PURL_with_namespace (0.00s)
--- PASS: TestParsePackageArg/PURL_ignores_ecosystem_flag (0.00s)
--- PASS: TestParsePackageArg/invalid_PURL_returns_error (0.00s)
--- PASS: TestParsePackageArg/gem_PURL_maps_to_rubygems_ecosystem (0.00s)
--- PASS: TestParsePackageArg/golang_PURL_with_namespace (0.00s)
PASS
ok github.com/git-pkgs/git-pkgs/cmd 0.270sAll unit tests pass. Now run the integration tests.
go test ./cmd/ -run "TestWhy(AcceptsPURL)|TestHistory(AcceptsPURL)|TestWhere(AcceptsPURL)|TestAdd(AcceptsPURL)|TestRemove(AcceptsPURL)|TestUpdate(AcceptsPURL)|TestBrowse(AcceptsPURL)" -v 2>&1=== RUN TestWhyAcceptsPURL
=== RUN TestWhyAcceptsPURL/accepts_PURL_argument
=== RUN TestWhyAcceptsPURL/PURL_produces_same_result_as_ecosystem_flag
=== RUN TestWhyAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestWhyAcceptsPURL (0.13s)
--- PASS: TestWhyAcceptsPURL/accepts_PURL_argument (0.00s)
--- PASS: TestWhyAcceptsPURL/PURL_produces_same_result_as_ecosystem_flag (0.01s)
--- PASS: TestWhyAcceptsPURL/invalid_PURL_returns_error (0.00s)
=== RUN TestHistoryAcceptsPURL
=== RUN TestHistoryAcceptsPURL/accepts_PURL_argument
=== RUN TestHistoryAcceptsPURL/PURL_produces_same_result_as_ecosystem_flag
=== RUN TestHistoryAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestHistoryAcceptsPURL (0.10s)
--- PASS: TestHistoryAcceptsPURL/accepts_PURL_argument (0.00s)
--- PASS: TestHistoryAcceptsPURL/PURL_produces_same_result_as_ecosystem_flag (0.01s)
--- PASS: TestHistoryAcceptsPURL/invalid_PURL_returns_error (0.00s)
=== RUN TestWhereAcceptsPURL
=== RUN TestWhereAcceptsPURL/accepts_PURL_argument
=== RUN TestWhereAcceptsPURL/PURL_produces_same_result_as_ecosystem_flag
=== RUN TestWhereAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestWhereAcceptsPURL (0.07s)
--- PASS: TestWhereAcceptsPURL/accepts_PURL_argument (0.00s)
--- PASS: TestWhereAcceptsPURL/PURL_produces_same_result_as_ecosystem_flag (0.00s)
--- PASS: TestWhereAcceptsPURL/invalid_PURL_returns_error (0.00s)
=== RUN TestAddAcceptsPURL
=== RUN TestAddAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestAddAcceptsPURL (0.00s)
--- PASS: TestAddAcceptsPURL/invalid_PURL_returns_error (0.00s)
=== RUN TestRemoveAcceptsPURL
=== RUN TestRemoveAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestRemoveAcceptsPURL (0.00s)
--- PASS: TestRemoveAcceptsPURL/invalid_PURL_returns_error (0.00s)
=== RUN TestUpdateAcceptsPURL
=== RUN TestUpdateAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestUpdateAcceptsPURL (0.00s)
--- PASS: TestUpdateAcceptsPURL/invalid_PURL_returns_error (0.00s)
=== RUN TestBrowseAcceptsPURL
=== RUN TestBrowseAcceptsPURL/invalid_PURL_returns_error
--- PASS: TestBrowseAcceptsPURL (0.00s)
--- PASS: TestBrowseAcceptsPURL/invalid_PURL_returns_error (0.00s)
PASS
ok github.com/git-pkgs/git-pkgs/cmd 0.593sAll passing. Now run the full test suite.
go test ./cmd/ -v 2>&1<persisted-output>
Output too large (65.6KB). Full output saved to: /Users/andrew/.claude/projects/-Users-andrew-code-git-pkgs/8469ea1b-abe5-4a16-9071-e531cc8f005c/tool-results/toolu_01Jz5RRjbM6b1RPy6iDVK6r4.txt
Preview (first 2KB):
=== RUN TestDiff_ManifestDeleted
--- PASS: TestDiff_ManifestDeleted (0.21s)
=== RUN TestDiff_ManifestDeletedBetweenCommits
--- PASS: TestDiff_ManifestDeletedBetweenCommits (0.36s)
=== RUN TestDiff_ManifestRenamed
--- PASS: TestDiff_ManifestRenamed (0.51s)
=== RUN TestDiff_TypeFilter
--- PASS: TestDiff_TypeFilter (0.20s)
=== RUN TestComputeDiff_Modified
--- PASS: TestComputeDiff_Modified (0.00s)
=== RUN TestComputeDiff_SamePackageDifferentManifests
--- PASS: TestComputeDiff_SamePackageDifferentManifests (0.00s)
=== RUN TestComputeDiff_DuplicatePackageVersionsInLockfile
--- PASS: TestComputeDiff_DuplicatePackageVersionsInLockfile (0.00s)
=== RUN TestComputeDiff_MultiVersionUpgrade
--- PASS: TestComputeDiff_MultiVersionUpgrade (0.00s)
=== RUN TestComputeDiff_VersionCountChangeWithUpgrade
--- PASS: TestComputeDiff_VersionCountChangeWithUpgrade (0.00s)
=== RUN TestComputeDiff_MultiVersionMixed
--- PASS: TestComputeDiff_MultiVersionMixed (0.00s)
=== RUN TestEcosystems
=== RUN TestEcosystems/text_output_includes_header_and_known_ecosystems
=== RUN TestEcosystems/json_output_is_valid_and_contains_ecosystems
--- PASS: TestEcosystems (0.00s)
--- PASS: TestEcosystems/text_output_includes_header_and_known_ecosystems (0.00s)
--- PASS: TestEcosystems/json_output_is_valid_and_contains_ecosystems (0.00s)
=== RUN TestBuildEcosystemDetails
--- PASS: TestBuildEcosystemDetails (0.00s)
=== RUN TestDoUninstallHooks_AppendedLines
post-commit: removed git-pkgs lines
--- PASS: TestDoUninstallHooks_AppendedLines (0.00s)
=== RUN TestDoUninstallHooks_BlankLineBetweenMarkers
post-commit: removed git-pkgs lines
--- PASS: TestDoUninstallHooks_BlankLineBetweenMarkers (0.00s)
=== RUN TestPagerFlagAccepted
=== RUN TestPagerFlagAccepted/blame
Show the commit and author that first added each current dependency.
Usage:
git-pkgs blame [flags]
Aliases:
blame, praise
Flags:
-b, --branch string Branch to query (default: first tracked branch)
...
</persisted-output>tail -5 /Users/andrew/.claude/projects/-Users-andrew-code-git-pkgs/8469ea1b-abe5-4a16-9071-e531cc8f005c/tool-results/toolu_01Jz5RRjbM6b1RPy6iDVK6r4.txt --- PASS: TestUrlsNameLookup/looks_up_package_by_name (0.12s)
--- PASS: TestUrlsNameLookup/errors_when_package_not_found (0.08s)
--- PASS: TestUrlsNameLookup/errors_when_no_database_exists (0.06s)
PASS
ok github.com/git-pkgs/git-pkgs/cmd 15.974sAll tests pass. Let me also run go vet.
go vet ./... 2>&1(empty result)
Updated task #4 status
Everything passes. Here's a summary of what was done:
New helper (cmd/helpers.go):
ParsePackageArg(arg, ecosystemFlag) that returns (ecosystem, name, version, error). When the arg is a PURL, it parses out the ecosystem, name, and version; when it's a plain name, it passes through the ecosystem flag.8 commands updated:
why, history, where -- call ParsePackageArg at the top, use returned ecosystem/name for database queriesvulns history -- added --ecosystem flag it was missing, call ParsePackageArg, filter the dep-matching loop by ecosystem when setbrowse, remove -- call ParsePackageArg, use returned ecosystem for FilterByEcosystem and name as the package argadd -- call ParsePackageArg, use PURL version as the version arg when no explicit version argument is given (so git-pkgs add pkg:npm/lodash@4.17.21 works like git-pkgs add lodash 4.17.21 -e npm)update -- call ParsePackageArg when a package arg is provided, use returned ecosystem for filteringTests (cmd/helpers_test.go):
TestParsePackageArg -- 9 cases covering plain names, PURLs with/without version, namespaced PURLs, ecosystem mapping, flag-ignored-when-PURL, invalid PURL errorwhy, history, where verifying PURL produces same output as --ecosystem flagopen a pr
Let me check the current state of the repo first.
cd /Users/andrew/code/git-pkgs && git statusExit code 128 fatal: not a git repository (or any of the parent directories): .git
cd /Users/andrew/code/git-pkgs && git log --oneline -5<tool_use_error>Sibling tool call errored</tool_use_error>
cd /Users/andrew/code/git-pkgs && git branch --show-current<tool_use_error>Sibling tool call errored</tool_use_error>
cd /Users/andrew/code/git-pkgs && git diff<tool_use_error>Sibling tool call errored</tool_use_error>
The parent git-pkgs directory isn't a git repo -- the actual repo must be in a subdirectory. Let me find it.
git status && echo "---" && git branch --show-current && echo "---" && git log --oneline -5On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: cmd/add.go modified: cmd/browse.go modified: cmd/helpers.go modified: cmd/history.go modified: cmd/remove.go modified: cmd/update.go modified: cmd/vulns.go modified: cmd/where.go modified: cmd/why.go Untracked files: (use "git add <file>..." to include in what will be committed) cmd/helpers_test.go no changes added to commit (use "git add" and/or "git commit -a") --- main --- a618c59 Merge pull request #107 from git-pkgs/urls-command 1af5b1c Merge pull request #106 from git-pkgs/resolve-command 9cb16ea Default resolve output to tree format, add -f json flag f36345c Add dependency chain example to resolve docs d27cd7e Add usage examples to resolve docs
git diff HEADdiff --git a/cmd/add.go b/cmd/add.go
index 2aed0f9..7c8671c 100644
--- a/cmd/add.go
+++ b/cmd/add.go
@@ -39,15 +39,21 @@ Examples:
}
func runAdd(cmd *cobra.Command, args []string) error {
- pkg := args[0]
+ dev, _ := cmd.Flags().GetBool("dev")
+ managerOverride, _ := cmd.Flags().GetString("manager")
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
+
+ ecosystem, pkg, purlVersion, err := ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
+
var version string
if len(args) > 1 {
version = args[1]
+ } else if purlVersion != "" {
+ version = purlVersion
}
-
- dev, _ := cmd.Flags().GetBool("dev")
- managerOverride, _ := cmd.Flags().GetString("manager")
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
dryRun, _ := cmd.Flags().GetBool("dry-run")
quiet, _ := cmd.Flags().GetBool("quiet")
extra, _ := cmd.Flags().GetStringArray("extra")
diff --git a/cmd/browse.go b/cmd/browse.go
index dbbeb38..d83266a 100644
--- a/cmd/browse.go
+++ b/cmd/browse.go
@@ -56,9 +56,13 @@ Examples:
}
func runBrowse(cmd *cobra.Command, args []string) error {
- pkg := args[0]
managerOverride, _ := cmd.Flags().GetString("manager")
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
+
+ ecosystem, pkg, _, err := ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
printPath, _ := cmd.Flags().GetBool("path")
openInBrowser, _ := cmd.Flags().GetBool("open")
timeout, _ := cmd.Flags().GetDuration("timeout")
diff --git a/cmd/helpers.go b/cmd/helpers.go
index 9a2ae61..b0f87a7 100644
--- a/cmd/helpers.go
+++ b/cmd/helpers.go
@@ -6,6 +6,7 @@ import (
"github.com/git-pkgs/git-pkgs/internal/database"
"github.com/git-pkgs/git-pkgs/internal/git"
+ "github.com/git-pkgs/purl"
)
func openDatabase() (*git.Repository, *database.DB, error) {
@@ -57,6 +58,20 @@ func IsPURL(s string) bool {
return strings.HasPrefix(s, "pkg:")
}
+func ParsePackageArg(arg, ecosystemFlag string) (ecosystem, name, version string, err error) {
+ if IsPURL(arg) {
+ p, err := purl.Parse(arg)
+ if err != nil {
+ return "", "", "", fmt.Errorf("parsing purl: %w", err)
+ }
+ ecosystem = purl.PURLTypeToEcosystem(p.Type)
+ name = p.FullName()
+ version = p.Version
+ return ecosystem, name, version, nil
+ }
+ return ecosystemFlag, arg, "", nil
+}
+
func filterByEcosystem(deps []database.Dependency, ecosystem string) []database.Dependency {
if ecosystem == "" {
return deps
diff --git a/cmd/history.go b/cmd/history.go
index f845c55..cc4d319 100644
--- a/cmd/history.go
+++ b/cmd/history.go
@@ -29,12 +29,18 @@ Changes are shown in chronological order.`,
}
func runHistory(cmd *cobra.Command, args []string) error {
- packageName := ""
- if len(args) > 0 {
- packageName = args[0]
- }
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
+ var packageName, ecosystem string
+ if len(args) > 0 {
+ var err error
+ ecosystem, packageName, _, err = ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
+ } else {
+ ecosystem = ecosystemFlag
+ }
author, _ := cmd.Flags().GetString("author")
since, _ := cmd.Flags().GetString("since")
until, _ := cmd.Flags().GetString("until")
diff --git a/cmd/remove.go b/cmd/remove.go
index 1f764e7..8efb9f6 100644
--- a/cmd/remove.go
+++ b/cmd/remove.go
@@ -38,10 +38,13 @@ Examples:
}
func runRemove(cmd *cobra.Command, args []string) error {
- pkg := args[0]
-
managerOverride, _ := cmd.Flags().GetString("manager")
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
+
+ ecosystem, pkg, _, err := ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
dryRun, _ := cmd.Flags().GetBool("dry-run")
quiet, _ := cmd.Flags().GetBool("quiet")
extra, _ := cmd.Flags().GetStringArray("extra")
diff --git a/cmd/update.go b/cmd/update.go
index 7a1e064..2aa3380 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -39,13 +39,19 @@ Examples:
}
func runUpdate(cmd *cobra.Command, args []string) error {
- var pkg string
- if len(args) > 0 {
- pkg = args[0]
- }
-
managerOverride, _ := cmd.Flags().GetString("manager")
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
+
+ var pkg, ecosystem string
+ if len(args) > 0 {
+ var err error
+ ecosystem, pkg, _, err = ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
+ } else {
+ ecosystem = ecosystemFlag
+ }
all, _ := cmd.Flags().GetBool("all")
dryRun, _ := cmd.Flags().GetBool("dry-run")
quiet, _ := cmd.Flags().GetBool("quiet")
diff --git a/cmd/vulns.go b/cmd/vulns.go
index 7479c1a..9cf0827 100644
--- a/cmd/vulns.go
+++ b/cmd/vulns.go
@@ -1413,6 +1413,7 @@ Shows when the package was vulnerable and what vulnerabilities affected it.`,
}
historyCmd.Flags().StringP("branch", "b", "", "Branch to query (default: first tracked branch)")
+ historyCmd.Flags().StringP("ecosystem", "e", "", "Filter by ecosystem")
historyCmd.Flags().Int("limit", 50, "Maximum commits to check")
historyCmd.Flags().StringP("format", "f", "text", "Output format: text, json")
parent.AddCommand(historyCmd)
@@ -1426,11 +1427,16 @@ type VulnHistoryEntry struct {
}
func runVulnsHistory(cmd *cobra.Command, args []string) error {
- packageName := args[0]
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
branchName, _ := cmd.Flags().GetString("branch")
limit, _ := cmd.Flags().GetInt("limit")
format, _ := cmd.Flags().GetString("format")
+ ecosystem, packageName, _, err := ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
+
_, db, err := openDatabase()
if err != nil {
return err
@@ -1466,6 +1472,9 @@ func runVulnsHistory(cmd *cobra.Command, args []string) error {
if !strings.EqualFold(d.Name, packageName) {
continue
}
+ if ecosystem != "" && !strings.EqualFold(d.Ecosystem, ecosystem) {
+ continue
+ }
if isResolvedDependency(d) {
pkgDep = &d
break
diff --git a/cmd/where.go b/cmd/where.go
index da73072..cbc7500 100644
--- a/cmd/where.go
+++ b/cmd/where.go
@@ -41,8 +41,12 @@ type WhereMatch struct {
}
func runWhere(cmd *cobra.Command, args []string) error {
- packageName := args[0]
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
+
+ ecosystem, packageName, _, err := ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
context, _ := cmd.Flags().GetInt("context")
format, _ := cmd.Flags().GetString("format")
includeSubmodules, _ := cmd.Flags().GetBool("include-submodules")
diff --git a/cmd/why.go b/cmd/why.go
index 189719a..0b1d704 100644
--- a/cmd/why.go
+++ b/cmd/why.go
@@ -25,10 +25,14 @@ func addWhyCmd(parent *cobra.Command) {
}
func runWhy(cmd *cobra.Command, args []string) error {
- packageName := args[0]
- ecosystem, _ := cmd.Flags().GetString("ecosystem")
+ ecosystemFlag, _ := cmd.Flags().GetString("ecosystem")
format, _ := cmd.Flags().GetString("format")
+ ecosystem, packageName, _, err := ParsePackageArg(args[0], ecosystemFlag)
+ if err != nil {
+ return err
+ }
+
repo, err := git.OpenRepository(".")
if err != nil {
return fmt.Errorf("not in a git repository: %w", err)Good. Let me also check the remote and read the README before creating the branch and PR.
git remote -vorigin https://github.com/git-pkgs/git-pkgs.git (fetch) origin https://github.com/git-pkgs/git-pkgs.git (push)
git log --oneline -3a618c59 Merge pull request #107 from git-pkgs/urls-command 1af5b1c Merge pull request #106 from git-pkgs/resolve-command 9cb16ea Default resolve output to tree format, add -f json flag
The user wants me to document the PURL support in the README. I need to add mentions of PURL support to the relevant command sections. Let me look at how urls documents it and follow that pattern.
Looking at the README, I need to add PURL examples to the sections for:
The vulnerability scanning section also needs PURL examples. I think the cleanest approach is to add PURL examples directly to each relevant command section rather than creating a separate subsection, since the urls command already demonstrates this pattern well. This keeps the documentation maintainable and the examples close to where users need them.
I'll add a brief PURL note early in the Quick start section to introduce the concept, then sprinkle concrete examples throughout the command sections that support it.
Now I need to add PURL documentation to the README. Let me add examples to the relevant command sections.
Now add a general note about PURL support. The urls section already explains PURLs. I'll add a note in the Configuration section about PURL arguments being accepted across commands.
Now let me make sure everything still builds and tests pass with the README changes, then create the branch, commit, and PR.
go build ./... && go test ./cmd/ -run TestParsePackageArg -count=1 2>&1ok github.com/git-pkgs/git-pkgs/cmd 0.248s
Now let me create the branch, commit, and push.
git statusOn branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: README.md modified: cmd/add.go modified: cmd/browse.go modified: cmd/helpers.go modified: cmd/history.go modified: cmd/remove.go modified: cmd/update.go modified: cmd/vulns.go modified: cmd/where.go modified: cmd/why.go Untracked files: (use "git add <file>..." to include in what will be committed) cmd/helpers_test.go no changes added to commit (use "git add" and/or "git commit -a")
git checkout -b purl-package-argsSwitched to a new branch 'purl-package-args'
git add README.md cmd/helpers.go cmd/helpers_test.go cmd/why.go cmd/history.go cmd/where.go cmd/vulns.go cmd/browse.go cmd/add.go cmd/remove.go cmd/update.go(empty result)
git commit -m "$(cat <<'EOF'
Accept PURLs as package arguments in more commands
Add ParsePackageArg helper that parses a PURL or passes through a plain
name with the ecosystem flag. Use it in why, history, where, browse, add,
remove, update, and vulns history so users can write
`git pkgs why pkg:npm/lodash` instead of `git pkgs why lodash -e npm`.
The add command also extracts the version from the PURL, so
`git pkgs add pkg:npm/lodash@4.17.21` works like
`git pkgs add lodash 4.17.21 -e npm`.
Adds --ecosystem flag to vulns history (was missing).
Closes #108
EOF
)"[purl-package-args 6902e47] Accept PURLs as package arguments in more commands 11 files changed, 412 insertions(+), 26 deletions(-) create mode 100644 cmd/helpers_test.go
git push -u origin purl-package-args 2>&1remote: remote: Create a pull request for 'purl-package-args' on GitHub by visiting: remote: https://github.com/git-pkgs/git-pkgs/pull/new/purl-package-args remote: To https://github.com/git-pkgs/git-pkgs.git * [new branch] purl-package-args -> purl-package-args branch 'purl-package-args' set up to track 'origin/purl-package-args'.
gh pr create --title "Accept PURLs as package arguments in more commands" --body "$(cat <<'EOF'
Adds a shared ParsePackageArg helper and uses it in 8 commands so users can write `git pkgs why pkg:npm/lodash` instead of `git pkgs why lodash -e npm`.
When the argument starts with `pkg:`, the ecosystem and name are extracted from the PURL and the `--ecosystem` flag is ignored. Plain names work as before.
Commands updated: `why`, `history`, `where`, `browse`, `add`, `remove`, `update`, `vulns history`.
The `add` command also extracts the version from the PURL, so `git pkgs add pkg:npm/lodash@4.17.21` works like `git pkgs add lodash 4.17.21 -e npm`.
Also adds the missing `--ecosystem` flag to `vulns history`.
Closes #108
EOF
)" 2>&1https://github.com/git-pkgs/git-pkgs/pull/119
PR created: https://github.com/git-pkgs/git-pkgs/pull/119
Implement the following plan:
The urls command already accepts PURLs (pkg:cargo/serde@1.0.0) as its package argument, parsing out the ecosystem, name, and version. Other commands that take a package name argument still require plain names plus a separate --ecosystem flag. This change extends PURL support to 8 more commands so users can write git-pkgs why pkg:npm/lodash instead of git-pkgs why lodash -e npm.
Add ParsePackageArg to cmd/helpers.go:
When the arg is a PURL, the ecosystem is derived from the PURL type and the --ecosystem flag is ignored. When it's a plain name, the ecosystem comes from the flag (may be empty) and version is empty.
These use ecosystem to filter database queries. The change is the same pattern: call ParsePackageArg at the top, use the returned ecosystem instead of the raw flag.
cmd/why.go (runWhy) - replace packageName := args[0] + ecosystem flag read with ParsePackageArg(args[0], ecosystem). Pass returned ecosystem and name to db.GetWhy().
cmd/history.go (runHistory) - same pattern. Pass ecosystem and name to db.GetPackageHistory().
cmd/where.go (runWhere) - same pattern. The returned ecosystem filters which manifest types to search. The name is used for the regex search.
cmd/vulns.go (runVulnsHistory, ~line 1428) - same pattern, but this command currently has no --ecosystem flag. Add one. Use the returned ecosystem to also filter the strings.EqualFold(d.Name, packageName) loop to also check ecosystem when set, avoiding false matches across ecosystems.
These use ecosystem to filter detected package managers. The change: call ParsePackageArg, use returned ecosystem for FilterByEcosystem, and use returned name as the package arg passed to the manager.
cmd/browse.go (runBrowse) - parse PURL, use name as pkg for mgr.Path(), use ecosystem for FilterByEcosystem.
cmd/add.go (runAdd) - parse PURL, use name as package and version from PURL (if present) as version arg. So git-pkgs add pkg:npm/lodash@4.17.21 works like git-pkgs add lodash 4.17.21 -e npm.
cmd/remove.go (runRemove) - parse PURL, use name as package, ecosystem for filtering.
cmd/update.go (runUpdate) - parse PURL when a package arg is provided, use name as package, ecosystem for filtering.
cmd/helpers.go - add ParsePackageArg, add purl importcmd/why.go - use ParsePackageArgcmd/history.go - use ParsePackageArgcmd/where.go - use ParsePackageArgcmd/vulns.go - add --ecosystem flag to vulns history, use ParsePackageArgcmd/browse.go - use ParsePackageArgcmd/add.go - use ParsePackageArg, use version from PURLcmd/remove.go - use ParsePackageArgcmd/update.go - use ParsePackageArgAdd to cmd/urls_test.go (or a new cmd/helpers_test.go since ParsePackageArg lives in helpers):
TestParsePackageArg - unit tests for the shared helper covering: PURL input extracts ecosystem/name/version, plain name passes through ecosystem flag, PURL with namespace, invalid PURL returns error, ecosystem flag ignored when PURL providedAdd integration tests for each command (following the urls_test.go pattern with createTestRepo/runCmd):
--ecosystem + plain namecd /Users/andrew/code/git-pkgs/git-pkgs && go build ./...go test ./cmd/ -run TestParsePackageArggo test ./cmd/ -v (full test suite)go vet ./... and golangci-lint run if availableIf you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/andrew/.claude/projects/-Users-andrew-code-git-pkgs/cadb53f4-f34f-4fcf-9066-e88933cb2fbc.jsonl
Now let me also look at the existing tests and the purl package to understand the purl.Parse, purl.PURLTypeToEcosystem, and purl.FullName() methods.
Now let me check the existing test patterns and the purl package.
I'll start by reading the key files to understand the current patterns before implementing.
Perfect! Now I have a comprehensive understanding of the PURL package API. Let me provide you with a complete summary.
## PURL Package API Reference
Here's the complete API for the purl package at `/Users/andrew/code/git-pkgs/purl/`:
### Core Struct
**PURL** - A wrapper around `packageurl.PackageURL` with helper methods
```go
type PURL struct {
packageurl.PackageURL // Embedded struct with Type, Namespace, Name, Version, Qualifiers, Subpath
}
```
### Parsing and Creation Functions
**Parse(s string) (*PURL, error)**
- Parses a Package URL string (e.g., "pkg:npm/%40babel/core@7.24.0") into a PURL
- Returns pointer to PURL and error
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:20-26`
**New(purlType, namespace, name, version string, qualifiers map[string]string) *PURL**
- Creates a PURL from individual components
- Qualifiers are sorted for deterministic output
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:29-44`
**MakePURL(ecosystem, name, version string) *PURL**
- Constructs a PURL from ecosystem-native package identifiers
- Handles automatic namespace extraction for npm (@scope/pkg), maven (group:artifact), golang (github.com/foo/bar), composer (vendor/package), alpine, arch, and github-actions
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:117-164`
**MakePURLString(ecosystem, name, version string) string**
- Like MakePURL but returns the PURL as a string directly
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:167-169`
### PURL Methods
**String() string**
- Returns the PURL as a formatted string
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:47-49`
**FullName() string**
- Returns the combined package name (namespace + name)
- Uses "/" separator for most ecosystems, ":" for maven
- Returns just the name if no namespace
- Located in `/Users/andrew/code/git-pkgs/purl/fullname.go:6-14`
**RepositoryURL() string**
- Returns the repository_url qualifier value if present
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:52-54`
**IsPrivateRegistry() bool**
- Returns true if the PURL has a non-default repository_url
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:57-63`
**Qualifier(key string) string**
- Returns the value of a qualifier, or empty string if not present
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:66-68`
**WithVersion(version string) *PURL**
- Returns a copy of the PURL with a different version
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:71-82`
**WithoutVersion() *PURL**
- Returns a copy of the PURL without a version
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:85-87`
**WithQualifier(key, value string) *PURL**
- Returns a copy of the PURL with a qualifier set or replaced
- Located in `/Users/andrew/code/git-pkgs/purl/purl.go:91-115`
### Ecosystem Conversion Functions
**PURLTypeToEcosystem(purlType string) string**
- Converts a PURL type back to an ecosystem name
- Inverse of EcosystemToPURLType
- Maps "apk" → "alpine", "alpm" → "arch", "gem" → "rubygems", "composer" → "packagist", "githubactions" → "github-actions"
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:79-87`
**EcosystemToPURLType(ecosystem string) string**
- Converts an ecosystem name to the corresponding PURL type
- Handles aliases like "go" → "golang", "gem" → "rubygems"
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:69-75`
**NormalizeEcosystem(ecosystem string) string**
- Returns the canonical ecosystem name
- Handles aliases like "go" → "golang", "gem" → "rubygems"
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:59-65`
**EcosystemToOSV(ecosystem string) string**
- Converts an ecosystem name to the OSV ecosystem name
- Example: "npm" → "npm", "rubygems" → "RubyGems"
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:91-97`
**PURLTypeToDepsdev(purlType string) string**
- Converts a PURL type to the deps.dev system name
- Returns empty string if not supported by deps.dev
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:101-106`
### Type Information Functions
**TypeInfo(purlType string) *TypeConfig**
- Returns configuration for a PURL type, or nil if unknown
- Located in `/Users/andrew/code/git-pkgs/purl/types.go:80-90`
**KnownTypes() []string**
- Returns a sorted list of all known PURL types
- Located in `/Users/andrew/code/git-pkgs/purl/types.go:93-104`
**IsKnownType(purlType string) bool**
- Returns true if the PURL type is defined in types.json
- Located in `/Users/andrew/code/git-pkgs/purl/types.go:107-114`
**DefaultRegistry(purlType string) string**
- Returns the default registry URL for a PURL type
- Empty string if type has no default registry
- Located in `/Users/andrew/code/git-pkgs/purl/types.go:118-124`
**IsValidEcosystem(ecosystem string) bool**
- Returns true if the ecosystem is recognized
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:205-209`
**SupportedEcosystems() []string**
- Returns a list of all supported ecosystem names
- Includes both PURL types and common aliases
- Located in `/Users/andrew/code/git-pkgs/purl/ecosystem.go:173-202`
### Type Structures
**TypeConfig** (from types.go)
```go
type TypeConfig struct {
Description string // Human-readable description
DefaultRegistry *string // Default registry URL
NamespaceRequirement string // "required", "prohibited", or "optional"
Examples []string // Example PURLs
RegistryConfig *RegistryConfig // URL generation configuration
}
// Methods:
func (t *TypeConfig) NamespaceRequired() bool // true if namespace is required
func (t *TypeConfig) NamespaceProhibited() bool // true if namespace is prohibited
```
### Version Cleaning
**CleanVersion(version, scheme string) string**
- Extracts a version from a version constraint string
- Uses the vers library to parse constraints and extract minimum bound
- Returns original string if parsing fails
- Located in `/Users/andrew/code/git-pkgs/purl/makepurl.go:12-28`
### Fast Path String Building
**BuildPURLString(ecosystem, name, version, registryURL string) string**
- Builds a PURL string directly without creating intermediate PURL structs
- Used for fast manifest parsing
- Handles private registry qualifiers
- Located in `/Users/andrew/code/git-pkgs/purl/makepurl.go:33-83`
All code is located in `/Users/andrew/code/git-pkgs/purl/` directory.
agentId: adb4467 (for resuming to continue this agent's work if needed)
<usage>total_tokens: 26239
tool_uses: 10
duration_ms: 24864</usage>Now let me read the test helper file to understand createTestRepo, runCmd, etc.
Let me read the test helpers and a few more files I need.
Now I have a good understanding of the codebase. Let me also quickly check what test fixtures exist for the test files (packageJSON, etc).
Let me check for test fixture data used across tests.
Found 6 files git-pkgs/cmd/urls_test.go git-pkgs/cmd/licenses_test.go git-pkgs/cmd/notes_test.go git-pkgs/cmd/analysis_test.go git-pkgs/cmd/query_test.go git-pkgs/cmd/database_test.go
No matches found
git-pkgs/cmd/query_test.go:16:const packageJSON = `{Good, I have all the context I need. Let me now implement the changes. I'll create tasks to track progress.
Task #1 created successfully: Add ParsePackageArg helper to cmd/helpers.go
Task #2 created successfully: Update 8 commands to use ParsePackageArg
Task #3 created successfully: Write tests for ParsePackageArg and command integration
Task #4 created successfully: Build, test, and verify
Updated task #1 status
Now let me add the ParsePackageArg helper to cmd/helpers.go.
The import will be used once we save; the diagnostic is transient since we just added the function. Let me verify it compiles.
Updated task #1 status
Updated task #2 status
Now let me update all 8 commands. I'll do the database-querying ones first, then the manager-operating ones.
1. cmd/why.go - replace packageName := args[0] + ecosystem flag read with ParsePackageArg:
2. cmd/history.go - same pattern:
3. cmd/where.go - same pattern:
4. cmd/vulns.go (runVulnsHistory) - add --ecosystem flag and use ParsePackageArg:
Now update the runVulnsHistory function to use ParsePackageArg and the ecosystem flag:
Now update the loop in runVulnsHistory that matches the package name to also filter by ecosystem when set:
Good, the ecosystem variable from ParsePackageArg is now used in the loop. Wait, the diagnostic says "declared and not used" - let me check. The ecosystem variable is declared in the ParsePackageArg call on line ~1435 and used in the loop. Let me re-check the file.
The code looks correct - ecosystem is used on line 1475. The diagnostic may have been stale. Let me now update the manager-operating commands.
5. cmd/browse.go - parse PURL:
6. cmd/add.go - parse PURL, use version from PURL:
7. cmd/remove.go - parse PURL: