Metadata-Version: 2.4
Name: libyear-multi
Version: 0.1.0
Summary: Language-agnostic libyear: dependency staleness scoring across PyPI, npm, crates.io, RubyGems and more.
Author: Shreyas Dhakal
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# libyear-multi

![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)

`libyear-multi` measures dependency staleness across the package managers used
by a project. It detects supported manifests, looks up release dates, and
reports a single score that can be used to prioritise maintenance work.

The [libyear](https://libyear.com/) metric measures dependency age in calendar
time rather than semantic-version distance. A package released three years ago
whose latest release shipped yesterday represents approximately three libyears
of staleness.

## Highlights

- Scans multiple ecosystems in one repository, including monorepos.
- Reports total, average, median, and maximum libyears.
- Classifies results into low, moderate, high, and severe risk bands.
- Provides both human-readable and JSON output.
- Caches registry responses locally to reduce repeated network requests.

## Supported Ecosystems

| Ecosystem | Manifest file | Registry |
| --- | --- | --- |
| Python | `requirements.txt` with pinned `==` versions | PyPI |
| Node.js | `package.json` | npm registry |
| Rust | `Cargo.lock` | crates.io |
| Ruby | `Gemfile.lock` | RubyGems |

Additional adapters are welcome. See [Adding an ecosystem](#adding-an-ecosystem).

## Installation

```bash
python -m pip install -e .
```

The package is not published to PyPI yet. Install it from a clone or use the
`libyear_multi/` package directly.

## Usage

### Command line

```bash
python -m libyear_multi.cli /path/to/project
```

Example output:

```text
14 dependencies scanned: 22.4 total libyears of staleness (avg 1.6 yrs/package, 21% severely outdated).
Risk band: high

Most outdated dependencies:
  [npm] left-pad: 1.1.0 -> 1.3.0  (4.2 libyears)
  [pypi] requests: 2.20.0 -> 2.32.3  (3.8 libyears)
```

Use JSON output for scripts and CI integrations:

```bash
python -m libyear_multi.cli /path/to/project --json
```

The number of concurrent registry lookups can be configured with
`--max-workers`.

### Python API

```python
from libyear_multi import LibyearScanner, score_dependency_age, summary_line

scanner = LibyearScanner()
dependencies = scanner.scan("/path/to/project")

score = score_dependency_age(dependencies)
print(summary_line(score))
print(score.to_risk_band())

for dependency in dependencies:
    print(dependency.name, dependency.ecosystem, dependency.libyears)
```

## How It Works

1. Each adapter checks whether its manifest exists in the target directory.
2. Detected adapters parse the manifest into package and installed-version pairs.
3. Registry APIs provide the latest version and release dates.
4. Staleness is calculated as `(latest_date - installed_date).days / 365.25`.
5. Individual results are aggregated into a project score.

Release dates are cached permanently in
`~/.cache/libyear-multi/cache.sqlite3`. Latest-version lookups expire after 24
hours.

## Development

Clone the repository and install the development dependencies:

```bash
git clone https://github.com/shreyasdhakal/libyear-multi.git
cd libyear-multi
python -m pip install -e ".[dev]"
```

Run the test suite:

```bash
python -m pytest
```

Before opening a pull request, make sure tests pass and that changes are
covered by tests where practical. Network-dependent registry calls are not part
of the unit test suite.

## Adding an Ecosystem

Subclass `EcosystemAdapter` in `libyear_multi/core.py` and implement its four
abstract methods:

```python
from datetime import datetime

from libyear_multi.core import EcosystemAdapter


class MyAdapter(EcosystemAdapter):
    name = "my_ecosystem"

    def detect(self, project_path: str) -> bool: ...
    def list_dependencies(self, project_path: str) -> list[tuple[str, str]]: ...
    def get_latest_version(self, name: str) -> str | None: ...
    def get_release_date(self, name: str, version: str) -> datetime | None: ...
```

Register the adapter in `libyear_multi/adapters/__init__.py` and add parsing
tests in `tests/`. Good future candidates include Go, Maven, Composer, and
NuGet.

## Known Limitations

- Python adapters currently expect pinned `==` versions.
- Rust and Ruby adapters read lockfiles, while npm currently reads `package.json`.
- Non-registry npm specifications such as Git URLs and workspace references are skipped.
- Transitive dependencies are only included when they appear in a parsed lockfile.
- Large repositories may encounter registry rate limits. The default worker limit is eight and responses are cached.
- The metric uses registry release dates and does not measure repository activity or abandonment.

## Contributing

Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before
submitting a change. Bug reports, documentation improvements, new adapters, and
focused fixes are all useful.

## Security

Please report suspected vulnerabilities privately by following the instructions
in [SECURITY.md](SECURITY.md). Do not disclose exploitable details in a public
issue.

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE).

Copyright (c) 2026 Shreyas Dhakal.
