📦 Package Info
Get metadata, versions, download statistics, and change tracking for a package on Packagist. This group of methods covers all "read" dimensions for a single package, foundation for dependency governance, mirror sync, monitoring alerts.
When to Use
- 📦 Dependency dashboard: Fetch package info (description, maintainers, stars, downloads) for all direct dependencies of a project for visualization.
- 🛠️ Mirror sync: Use
GetPackageChangesfor incremental sync to self-hosted mirror, avoid full fetch each time. - 📊 Download monitoring: Periodically record
GetPackageStatsdaily/monthly downloads, plot trends. - 🧩 Version detection: Use
GetPackageWithV2Metadata/GetPackageDevVersionsto get complete version tree needed by Composer V2 resolver (including dev branches).
Data Models
PackageInfo
Defined in pkg/domain/package.go, corresponds to content of package field in https://packagist.org/packages/{name}.json response.
type PackageInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Time time.Time `json:"time"`
Maintainers []*Maintainer `json:"maintainers"`
Versions map[string]*Version `json:"versions"`
Type string `json:"type"`
Repository string `json:"repository"`
GithubStars int `json:"github_stars"`
GithubWatchers int `json:"github_watchers"`
GithubForks int `json:"github_forks"`
GithubOpenIssues int `json:"github_open_issues"`
Language string `json:"language"`
Dependents int `json:"dependents"`
Suggesters int `json:"suggesters"`
Downloads PackageDownloads `json:"downloads"`
Favers int `json:"favers"`
}| Field | Type | Description |
|---|---|---|
Name | string | Package name (vendor/package) |
Description | string | Package description |
Time | time.Time | Package info last update time |
Maintainers | []*Maintainer | Maintainer list (Name, AvatarURL) |
Versions | map[string]*Version | Version number → version details, key like "v6.4.0" |
Type | string | Package type, e.g., library, composer-plugin |
Repository | string | Source code repository URL |
GithubStars etc. | int | Repository star / watcher / fork / open issue counts |
Language | string | Repository primary language |
Dependents | int | How many other packages depend on it |
Suggesters | int | Suggest install count |
Downloads | PackageDownloads | Download statistics (see below) |
Favers | int | Favorites count |
PackageDownloads
type PackageDownloads struct {
Total int `json:"total"`
Monthly int `json:"monthly"`
Daily int `json:"daily"`
}| Field | Type | Description |
|---|---|---|
Total | int | Historical total downloads |
Monthly | int | This month downloads |
Daily | int | Today downloads |
ComposerPackageInfo
Top-level structure returned by GetPackage, wraps PackageInfo with another layer, adding package name, timestamp and other localized fields:
type ComposerPackageInfo struct {
PackageName string `json:"package_name"`
PackageNameLowercase string `json:"package_name_lowercase"`
Package PackageInfo `json:"package"`
PackageInfoMd5 string `json:"package_info_md5"`
CreateTime *time.Time `json:"create_time"`
UpdateTime *time.Time `json:"update_time"`
ChangeTime *time.Time `json:"change_time"`
}| Field | Type | Description |
|---|---|---|
PackageName | string | Package name (same as requested) |
PackageNameLowercase | string | Lowercase package name, for case-insensitive queries |
Package | PackageInfo | Actual package info |
PackageInfoMd5 | string | Info MD5, for detecting changes (SDK currently doesn't auto-fill) |
CreateTime / UpdateTime / ChangeTime | *time.Time | Create / update / change timestamps (GetPackage fills current time to first two) |
PackageStatsResponse
pkg/domain/package_stats.go, corresponds to /packages/{name}/stats.json.
type PackageStatsResponse struct {
Downloads PackageDownloads `json:"downloads"`
Versions []string `json:"versions"`
Date string `json:"date"`
}| Field | Type | Description |
|---|---|---|
Downloads | PackageDownloads | Download statistics (total/monthly/daily) |
Versions | []string | Available versions list |
Date | string | Statistics start date |
ChangeTrackingResponse / ChangeAction
pkg/domain/package_stats.go, corresponds to /metadata/changes.json.
type ChangeTrackingResponse struct {
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Actions []ChangeAction `json:"actions,omitempty"`
}
type ChangeAction struct {
Type string `json:"type"`
Package string `json:"package"`
Time int64 `json:"time"`
}| Field | Type | Description |
|---|---|---|
Error | string | Error message returned when since parameter missing or invalid |
Timestamp | int64 | Current timestamp, as cursor for next incremental sync |
Actions | []ChangeAction | Change actions list |
Actions[].Type | string | Action type: update or delete |
Actions[].Package | string | Package name that changed |
Actions[].Time | int64 | Action occurrence time Unix timestamp |
GetPackage
📦 Get complete info for a specified package (including versions, maintainers, download statistics, GitHub metrics). Corresponds to GET https://packagist.org/packages/{name}.json.
Signature
func (c *ComposerClient) GetPackage(packageName string) (*domain.ComposerPackageInfo, error)Parameters
| Parameter | Type | Description |
|---|---|---|
packageName | string | Package name, format vendor/package, e.g., symfony/console |
Return Values
| Value | Type | Description |
|---|---|---|
| Result | *domain.ComposerPackageInfo | Package info, Package field contains detailed metadata |
| Error | error | Returned on HTTP failure, non-200, or JSON parsing failure |
Example
package main
import (
"fmt"
"log"
"time"
"github.com/scagogogo/composer-skills/pkg/client"
)
func main() {
c := client.NewComposerClient(30 * time.Second)
info, err := c.GetPackage("symfony/console")
if err != nil {
log.Fatalf("Failed to get package info: %v", err)
}
fmt.Printf("Package name: %s\n", info.PackageName)
fmt.Printf("Description: %s\n", info.Package.Description)
fmt.Printf("Type: %s\n", info.Package.Type)
fmt.Printf("GitHub stars: %d\n", info.Package.GithubStars)
fmt.Printf("Total downloads: %d (this month %d, today %d)\n",
info.Package.Downloads.Total,
info.Package.Downloads.Monthly,
info.Package.Downloads.Daily,
)
fmt.Printf("Version count: %d\n", len(info.Package.Versions))
for v := range info.Package.Versions {
fmt.Println(" -", v)
}
}Response Parsing
/packages/{name}.json returns {"package": {...}} outer wrapper. GetPackage internally first extracts to wrapper structure to get PackageInfo, then wraps as ComposerPackageInfo, and fills CreateTime / UpdateTime with current time, convenient for direct database storage.
GetPackageWithV2Metadata
📦 Get package info in Composer V2 metadata format (raw JSON bytes). Corresponds to GET https://repo.packagist.org/p2/{name}.json.
Signature
func (c *ComposerClient) GetPackageWithV2Metadata(packageName string) ([]byte, error)Parameters
| Parameter | Type | Description |
|---|---|---|
packageName | string | Package name, e.g., symfony/console |
Return Values
| Value | Type | Description |
|---|---|---|
| Data | []byte | V2 metadata raw JSON bytes |
| Error | error | Returned on HTTP failure, non-200 |
Example
data, err := c.GetPackageWithV2Metadata("symfony/console")
if err != nil {
log.Fatal(err)
}
// Parse yourself by V2 schema, or save directly to disk
fmt.Println(string(data))Why Return []byte?
V2 metadata structure is complex (contains packages.{name}.{version} multi-level nesting and minified fields), different users care about different fields, so SDK doesn't force modeling, leaves raw JSON to caller to parse as needed.
GetPackageDevVersions
📦 Get package development versions (branch versions) info (raw JSON bytes). Corresponds to GET https://repo.packagist.org/p2/{name}~dev.json.
Signature
func (c *ComposerClient) GetPackageDevVersions(packageName string) ([]byte, error)Parameters
| Parameter | Type | Description |
|---|---|---|
packageName | string | Package name, e.g., symfony/console |
Return Values
| Value | Type | Description |
|---|---|---|
| Data | []byte | Dev version metadata raw JSON bytes |
| Error | error | Returned on HTTP failure, non-200 |
Example
data, err := c.GetPackageDevVersions("symfony/console")
if err != nil {
log.Fatal(err)
}
// Contains dev-master / dev-main etc. branch versions
fmt.Println(string(data))URL Convention
Packagist uses appending ~dev to package name to request development branch versions, SDK automatically appends as {repoURL}/p2/{name}~dev.json.
GetPackageStats
📦 Get package download statistics and available versions list. Corresponds to GET https://packagist.org/packages/{name}/stats.json.
Signature
func (c *ComposerClient) GetPackageStats(packageName string) (*domain.PackageStatsResponse, error)Parameters
| Parameter | Type | Description |
|---|---|---|
packageName | string | Package name, e.g., symfony/console |
Return Values
| Value | Type | Description |
|---|---|---|
| Result | *domain.PackageStatsResponse | Contains download statistics, versions list, statistics date |
| Error | error | Returned on HTTP failure, non-200, or JSON parsing failure |
Example
stats, err := c.GetPackageStats("symfony/console")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total downloads: %d\n", stats.Downloads.Total)
fmt.Printf("This month downloads: %d\n", stats.Downloads.Monthly)
fmt.Printf("Today downloads: %d\n", stats.Downloads.Daily)
fmt.Printf("Available versions count: %d\n", len(stats.Versions))
fmt.Printf("Statistics start date: %s\n", stats.Date)GetPackageChanges
📦 Get incremental change records for package metadata. Corresponds to GET https://packagist.org/metadata/changes.json?since={timestamp}.
Signature
func (c *ComposerClient) GetPackageChanges(ctx context.Context, since int64) (*domain.ChangeTrackingResponse, error)Parameters
| Parameter | Type | Description |
|---|---|---|
ctx | context.Context | Context for timeout and cancellation |
since | int64 | Incremental cursor (Unix timestamp); pass 0 returns current timestamp but Actions empty, used to get initial cursor |
Return Values
| Value | Type | Description |
|---|---|---|
| Result | *domain.ChangeTrackingResponse | Contains Timestamp (next cursor) and Actions change list |
| Error | error | Returned on HTTP failure, non-200, or JSON parsing failure |
Example
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Initial sync: get a starting cursor
resp, err := c.GetPackageChanges(ctx, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Starting cursor: %d\n", resp.Timestamp)
// Next time use this cursor for incremental
resp, err = c.GetPackageChanges(ctx, resp.Timestamp)
if err != nil {
log.Fatal(err)
}
for _, a := range resp.Actions {
fmt.Printf("[%s] %s @ %d\n", a.Type, a.Package, a.Time)
}
fmt.Printf("Next cursor: %d\n", resp.Timestamp)Incremental Sync Mode
When since=0, only returns current Timestamp without historical changes. Correct approach: ① First use since=0 to get starting Timestamp and persist; ② Each subsequent time use previous Timestamp as since, process returned Actions, then save new Timestamp. If since invalid, response Error field will have error description.
Advanced Topics
Typical Mirror Sync Flow
Combine GetPackageChanges + GetPackage to build a lightweight self-hosted mirror:
- Initial:
GetPackageChanges(ctx, 0)get starting cursorT0. - Periodically:
GetPackageChanges(ctx, T0)getActions, for eachType=updatepackage callGetPackageto refresh local cache, forType=deletepackage remove from local. - Store response
Timestampas newT0, enter next round.
🔗 Related
- 📋 Batch get package names see Package Listing.
- 🛠️ Write operations (create/edit/update package) see Package Management.