devkit / composer-link
Link local Composer packages via path repositories. Composer plugin: `composer link`, `composer add`, `link-help`, `local-bootstrap`, `local-install`, and related commands.
Package info
github.com/Half-Shell-Studios/devkit-composer-link
Type:composer-plugin
pkg:composer/devkit/composer-link
Fund package maintenance!
Requires
- php: ^8.5
- composer-plugin-api: ^2.2
- composer/semver: ^3.4
- symfony/console: ^6.4|^7.0
- symfony/filesystem: ^6.4|^7.0
- symfony/process: ^6.4|^7.0
Requires (Dev)
- composer/composer: ^2.7
- laravel/pint: ^1.27
- pestphp/pest: ^3.0
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^1.12
- rector/rector: ^1.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
devkit-composer-link
Local packages. Fewer emotional incidents.
Package: devkit/composer-link
Table of Contents
- Why this exists
- How it works
- Use case — test a package without wrecking main
- Prerequisites
- Install
- Commands at a glance
- Configuration
- Files and folders
- Commands
- Typical workflows
- Version control and safety
- Support
- Changelog
- License
Why this exists
You know the dance.
You need to change a package. You open the app’s composer.json, add a path repository, force a constraint, run Composer, test the thing, then carefully undo everything so main doesn’t ship ../Desktop/my-fork forever.
It’s fine once. It’s miserable every day.
Composer Link exists so you can point dependencies at real folders without hand-editing the committed manifest like it’s 2004 — and come back tomorrow with the same setup still waiting for you.
How it works
Composer Link is a Composer plugin installed in your app. It keeps two worlds separate:
| Shared with the team | Yours only (gitignore these) |
|---|---|
composer.json |
packages-local.json — link state |
composer.lock |
composer.local.json — local manifest with path repos |
composer.local.lock — lock for that local manifest |
Commands like link / add write to the local files. CI and teammates keep using the committed baseline. Locally you run composer local-install (or set COMPOSER=composer.local.json) so vendor/ matches your checkout.
When you’re done experimenting, unlink or promote puts things back on a published constraint — without the archaeology.
Run everything from your application root (where the committed
composer.jsonlives). The plugin only registers whendevkit/composer-linkis installed as a dependency of that app.
Use case — test a package without wrecking main
Imagine your app depends on your-vendor/payments: ^1.5. You’ve got a local checkout with a bugfix. You want to prove it in the real app before you tag.
# once per clone (or first link/add creates it for you) composer local-bootstrap # point the existing dependency at your checkout composer link your-vendor/payments ../packages/payments # install against the local manifest composer local-install # sanity check composer linked composer link-doctor
Need @dev because the local branch doesn’t satisfy ^1.5?
composer link your-vendor/payments ../packages/payments --constraint=@dev
Bootstrapping something that isn’t in the committed manifest yet?
composer add your-vendor/new-thing ../packages/new-thing
Later, when it’s on Packagist:
composer promote your-vendor/new-thing ^1.0
A whole folder of checkouts? Scan them:
composer link-scan /opt/packages --vendor=your-vendor
Prerequisites
| Requirement | Version |
|---|---|
| PHP | 8.5+ |
| Composer | 2.2+ (plugin allow-list) |
Install
Install in the consuming application (not inside the library you’re editing):
composer require --dev devkit/composer-link
Allow the plugin (Composer 2.2+):
{
"config": {
"allow-plugins": {
"devkit/composer-link": true
}
}
}
Then:
composer link-help
Commands at a glance
| Command | In one sentence |
|---|---|
link |
Override an existing dependency to a local path. |
add |
Bootstrap a local package that isn’t in the committed manifest yet. |
unlink |
Drop local override state; restore or remove the requirement. |
promote |
Switch a locally managed package to a published constraint. |
linked |
List everything Composer Link is currently managing. |
refresh |
Rebuild managed path repos from packages-local.json. |
link-doctor |
Check ignore rules, paths, and managed repositories. |
local-bootstrap |
Copy committed manifest/lock into local files. |
local-install |
composer install via composer.local.json. |
link-scan |
Discover matching packages in a directory and link / add them. |
link-help |
Terminal cheat sheet for arguments and options. |
Configuration
Optional settings live under extra.composer-link in the app’s root composer.json:
{
"extra": {
"composer-link": {
"overrides_file": "packages-local.json",
"local_composer_json": "composer.local.json"
}
}
}
| Key | Role |
|---|---|
overrides_file |
Local state file (packages, paths, mode, constraints). |
local_composer_json |
Local Composer manifest used for path installs. |
Files and folders
packages-local.json <- plugin state (gitignore)
composer.local.json <- local manifest + path repos (gitignore)
composer.local.lock <- local lockfile (gitignore)
composer.json <- committed baseline
composer.lock <- committed baseline
Legacy note: if packages-local.json is missing/empty, composer.local-packages.json is still read as a fallback; the next write saves to packages-local.json.
Commands
Use composer help <command> for full flag docs.
link — override an existing dependency
composer link my-vendor/my-package ../packages/my-package composer link my-vendor/my-package ../packages/my-package --constraint=@dev composer link my-vendor/my-package ../packages/my-package --no-update composer link my-vendor/my-package ../packages/my-package --no-symlink
add — bootstrap a new local dependency
composer add my-vendor/new-lib ./libs/new-lib composer add my-vendor/new-lib ./libs/new-lib --no-dev composer add my-vendor/new-lib ./libs/new-lib --constraint=^0.1
unlink — stop managing a package locally
composer unlink my-vendor/my-package
composer unlink my-vendor/experimental-package --remove # required for bootstrap packages
promote — move to a published constraint
composer promote my-vendor/my-package ^1.5
composer promote my-vendor/my-package ~2.3.0 --no-update
linked — show managed packages
composer linked
refresh — rebuild managed path repos
composer refresh composer refresh --no-update
link-doctor — validate setup
composer link-doctor
Checks ignore rules, linked path existence, and managed path-repo count.
local-bootstrap — create local manifest files
composer local-bootstrap composer local-bootstrap --force
local-install — install via local manifest
composer local-install composer local-install --no-dev composer local-install --prefer-dist
Equivalent to:
COMPOSER=composer.local.json composer install
link-scan — discover and link a directory of checkouts
composer link-scan /opt/packages composer link-scan ../ --vendor=halfshell --vendor=halfshellstudios
Each matching package is linked if it’s already in the committed manifest, or added as require-dev if it isn’t. The host project itself is skipped.
link-help — command summary in terminal
composer link-help
Typical workflows
Override a released package
composer link your-vendor/your-package ../packages/your-package
Bootstrap before Packagist
composer add your-vendor/new-package ../packages/new-package
composer promote your-vendor/new-package ^1.0 # later
Day-to-day local loop
composer local-bootstrap composer link vendor/package ../path/to/package composer local-install
Version control and safety
Commit the shared baseline. Keep local override artifacts out of Git.
packages-local.json is Composer Link state only — Composer itself reads whichever manifest COMPOSER points at.
Before merging release work, align committed composer.json / composer.lock with the published constraints the team should install (promote, unlink, or normal edits).
Support
If this project saves you time (and a few Friday-afternoon incidents):
Changelog
See CHANGELOG.md for release notes by tag.