puli/cli
The command-line interface for Puli, the PHP package discovery and management system. Inspect packages, resolve resource paths, manage repositories, and integrate package discovery into your PHP projects from the terminal.
puli/cli installs the puli binary that manages a puli.json file and the .puli/ repository cache. Puli gives your application a virtual filesystem of paths (e.g. /app/assets/logo.png, /acme/blog/views/index.twig) that resolve to real files wherever Composer put them — this is what the CLI is for.
Minimal setup:
# project-local (recommended)
composer require --dev puli/cli
# the binary lands here
vendor/bin/puli --help
Initialize a project once:
vendor/bin/puli init # writes puli.json, asks for package name
vendor/bin/puli map /app/assets public/assets
vendor/bin/puli build # generates the repository cache in .puli/
Look at these first, in order:
puli.json — the only source of truth; bindings, mappings and config live here.vendor/bin/puli help and puli help <command> — the command set is small (init, map, bind, publish, build, type, resolve)..puli/repository.php — the generated cache your runtime code loads.First real use case: you have assets under public/ (or web/) that live inside vendor/ packages, and you want them addressable by a stable Puli path instead of hard-coded relative filenames. puli map /app/assets public/assets + puli publish /app/assets public/assets gets you there in two commands.
Map, don't hard-code filesystem paths. Every directory or file you want to reach by Puli path needs a map entry. Do this per package:
puli map /acme/blog/views src/Acme/Blog/Resources/views
puli map /acme/blog/public src/Acme/Blog/Resources/public
Bindings are how vendor packages become addressable. A package only appears by its Puli name if it has its own puli.json and is bound:
puli bind acme/blog vendor/acme/blog
Once bound, /acme/blog/... resolves regardless of Composer's install path.
Publishing is a deployment step, not a build step. For web-exposed assets:
puli publish /app/assets public/assets # copy
puli publish /app/assets public/assets --symlink # symlink (Linux/macOS)
puli publish --force /app/assets public/assets # overwrite
Use --symlink in dev so edits are live; use copy in production so the webroot is self-contained. Publish is idempotent enough to run every release.
Rebuild after every config change. puli build regenerates .puli/repository.php. Any script that mutates mappings must end with a build:
puli map /app/docs docs && puli build
Wire it into Composer scripts so the cache never goes stale:
"scripts": {
"post-install-cmd": ["puli build"],
"post-update-cmd": ["puli build"],
"post-autoload-dump": ["puli build"]
}
This also matters because the Composer plugin discovers packages during install — bindings added by a dependency are only visible after a fresh build.
Runtime lookup stays tiny. Application code should only resolve paths, not know about puli.json:
$puli = Puli\Manager\Api\Puli::create();
$repository = $puli->getRepository();
$file = $repository->get('/acme/blog/views/index.twig')->getFilesystemPath();
Custom resource types. If you need to load resources from a non-filesystem backend (database, remote), puli type registers the resource type and the corresponding loader/factory class, keeping the path abstraction consistent across the app. This is the main legitimate extension point of the CLI.
Team workflow: commit puli.json; gitignore .puli/. Let CI run puli build before tests so the cache matches the checked-in config.
files autoload, or plain realpath() calls cover most of what Puli did.puli build before touching anything else. The repository is a serialized snapshot, not a live view of the filesystem.puli.json is per-project. Puli bindings from dependencies are only picked up because puli/composer-plugin runs discovery during composer install/update. Run a plain composer dump-autoload without the plugin and discovery silently does nothing.--symlink breaks on Windows and on filesystems that don't support links. Default to copy in CI, symlink only on dev machines.vendor/ and you'll wonder why the browser 404s./app/assets and /app/assets/img), the more specific mapping wins for resources under it, but the merge order can surprise you. Keep mappings non-overlapping where possible; puli resolve /app/assets/img/x.png shows the final target and is your debugger.puli resolve and -vvv. puli resolve <path> prints the filesystem target; puli -vvv <cmd> explains discovery and build steps. Both beat reading the cache file by hand.puli.json overrides the global ~/.puli/puli.json. If a binding appears that you never declared, check whether a dependency's puli.json or a global config is supplying it..puli/: the cache is written at build time and read at runtime by the web user. Build as the deploy user (or the same UID that serves requests) or you'll hit unreadable-cache errors only in production.puli/cli belongs in require-dev; the runtime only needs puli/manager (and the repository cache). Shipping the CLI to production adds a binary and a plugin you'll never invoke.How can I help you explore Laravel packages today?