Skip to content

📦 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 GetPackageChanges for incremental sync to self-hosted mirror, avoid full fetch each time.
  • 📊 Download monitoring: Periodically record GetPackageStats daily/monthly downloads, plot trends.
  • 🧩 Version detection: Use GetPackageWithV2Metadata / GetPackageDevVersions to 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.

go
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"`
}
FieldTypeDescription
NamestringPackage name (vendor/package)
DescriptionstringPackage description
Timetime.TimePackage info last update time
Maintainers[]*MaintainerMaintainer list (Name, AvatarURL)
Versionsmap[string]*VersionVersion number → version details, key like "v6.4.0"
TypestringPackage type, e.g., library, composer-plugin
RepositorystringSource code repository URL
GithubStars etc.intRepository star / watcher / fork / open issue counts
LanguagestringRepository primary language
DependentsintHow many other packages depend on it
SuggestersintSuggest install count
DownloadsPackageDownloadsDownload statistics (see below)
FaversintFavorites count

PackageDownloads

go
type PackageDownloads struct {
    Total   int `json:"total"`
    Monthly int `json:"monthly"`
    Daily   int `json:"daily"`
}
FieldTypeDescription
TotalintHistorical total downloads
MonthlyintThis month downloads
DailyintToday downloads

ComposerPackageInfo

Top-level structure returned by GetPackage, wraps PackageInfo with another layer, adding package name, timestamp and other localized fields:

go
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"`
}
FieldTypeDescription
PackageNamestringPackage name (same as requested)
PackageNameLowercasestringLowercase package name, for case-insensitive queries
PackagePackageInfoActual package info
PackageInfoMd5stringInfo MD5, for detecting changes (SDK currently doesn't auto-fill)
CreateTime / UpdateTime / ChangeTime*time.TimeCreate / update / change timestamps (GetPackage fills current time to first two)

PackageStatsResponse

pkg/domain/package_stats.go, corresponds to /packages/{name}/stats.json.

go
type PackageStatsResponse struct {
    Downloads PackageDownloads `json:"downloads"`
    Versions  []string         `json:"versions"`
    Date      string           `json:"date"`
}
FieldTypeDescription
DownloadsPackageDownloadsDownload statistics (total/monthly/daily)
Versions[]stringAvailable versions list
DatestringStatistics start date

ChangeTrackingResponse / ChangeAction

pkg/domain/package_stats.go, corresponds to /metadata/changes.json.

go
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"`
}
FieldTypeDescription
ErrorstringError message returned when since parameter missing or invalid
Timestampint64Current timestamp, as cursor for next incremental sync
Actions[]ChangeActionChange actions list
Actions[].TypestringAction type: update or delete
Actions[].PackagestringPackage name that changed
Actions[].Timeint64Action 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

go
func (c *ComposerClient) GetPackage(packageName string) (*domain.ComposerPackageInfo, error)

Parameters

ParameterTypeDescription
packageNamestringPackage name, format vendor/package, e.g., symfony/console

Return Values

ValueTypeDescription
Result*domain.ComposerPackageInfoPackage info, Package field contains detailed metadata
ErrorerrorReturned on HTTP failure, non-200, or JSON parsing failure

Example

go
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

go
func (c *ComposerClient) GetPackageWithV2Metadata(packageName string) ([]byte, error)

Parameters

ParameterTypeDescription
packageNamestringPackage name, e.g., symfony/console

Return Values

ValueTypeDescription
Data[]byteV2 metadata raw JSON bytes
ErrorerrorReturned on HTTP failure, non-200

Example

go
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

go
func (c *ComposerClient) GetPackageDevVersions(packageName string) ([]byte, error)

Parameters

ParameterTypeDescription
packageNamestringPackage name, e.g., symfony/console

Return Values

ValueTypeDescription
Data[]byteDev version metadata raw JSON bytes
ErrorerrorReturned on HTTP failure, non-200

Example

go
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

go
func (c *ComposerClient) GetPackageStats(packageName string) (*domain.PackageStatsResponse, error)

Parameters

ParameterTypeDescription
packageNamestringPackage name, e.g., symfony/console

Return Values

ValueTypeDescription
Result*domain.PackageStatsResponseContains download statistics, versions list, statistics date
ErrorerrorReturned on HTTP failure, non-200, or JSON parsing failure

Example

go
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

go
func (c *ComposerClient) GetPackageChanges(ctx context.Context, since int64) (*domain.ChangeTrackingResponse, error)

Parameters

ParameterTypeDescription
ctxcontext.ContextContext for timeout and cancellation
sinceint64Incremental cursor (Unix timestamp); pass 0 returns current timestamp but Actions empty, used to get initial cursor

Return Values

ValueTypeDescription
Result*domain.ChangeTrackingResponseContains Timestamp (next cursor) and Actions change list
ErrorerrorReturned on HTTP failure, non-200, or JSON parsing failure

Example

go
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:

  1. Initial: GetPackageChanges(ctx, 0) get starting cursor T0.
  2. Periodically: GetPackageChanges(ctx, T0) get Actions, for each Type=update package call GetPackage to refresh local cache, for Type=delete package remove from local.
  3. Store response Timestamp as new T0, enter next round.

Released under the MIT License