Sync Command¶
The sync command reconciles your development environment with the desired state declared in a manifest file.
Manifest Files¶
Porringer looks for manifests in this order:
porringer.json- JSON manifest filepyproject.toml-[tool.porringer]section
Manifest Schema¶
| Field | Type | Description |
|---|---|---|
version |
string | Schema version (currently "1") |
packages |
object | Packages to install per ecosystem (e.g. {"python": ["requests"]}) |
tools |
object | CLI tools to install per ecosystem (e.g. {"python": ["pdm"]}) |
projects |
object | Project sync targets per ecosystem (e.g. {"python": []}) |
runtimes |
object | Language runtimes per ecosystem (e.g. {"python": ["3.12"]}) |
preferences |
object | Optional installer overrides per ecosystem (e.g. {"python": "uv"}) |
post_sync |
array | Commands to run after synchronisation |
name |
string | Optional human-readable project name |
description |
string | Optional short description |
author |
string | Optional author or organisation name |
url |
string | Optional project URL |
Kind Sections¶
The manifest groups entries by kind (packages, tools, projects, runtimes), each containing a dict keyed by ecosystem (e.g. "python", "node", "system"). Ecosystem names are free-form strings declared by plugins — adding a new ecosystem requires zero core changes.
packages— install individual packages (e.g.ruff,typescript).tools— install CLI tools in isolated environments (e.g.pdmviapipx).projects— synchronise an entire project's dependencies from its lock file.runtimes— install language runtimes (e.g. Python 3.12 viapim/pyenv).
Well-known ecosystems and their default installers:
| Ecosystem | Kind | Description | Default installers |
|---|---|---|---|
python |
packages | Python packages | uv, pip |
python |
tools | Python CLI tools | pipx |
python |
runtimes | Python runtimes | pim, pyenv |
python |
projects | Python project sync | uv, pdm, poetry |
system |
packages | OS-level packages | brew, apt, winget |
node |
packages | Node.js packages | npm, pnpm, bun |
node |
projects | Node.js project sync | npm, pnpm, yarn, bun |
deno |
packages | Deno packages | deno |
deno |
projects | Deno project sync | deno |
Preferences¶
Override the default installer selection per ecosystem:
Sync Strategies¶
| Strategy | Flag | Behaviour |
|---|---|---|
minimal |
(default) | Install missing packages; leave already-installed ones alone |
latest |
--strategy latest |
Upgrade every package to its latest allowed version |
exact |
--strategy exact |
Ensure each package satisfies the declared constraint |
Usage¶
Sync current directory:
Sync a specific directory:
Dry Run (Preview)¶
Preview actions without executing:
Dry-run mode previews all actions, including project-sync commands.
Project-sync backends that support --dry-run (e.g. pdm, uv) receive the
flag so they can report what they would do without modifying the environment.
Execution Phases¶
The sync engine processes actions in six ordered phases:
| Phase | Kind | Description |
|---|---|---|
| 1 | runtimes |
Install language runtimes (e.g. Python via pim/pyenv) |
| 2a | packages |
Install packages (e.g. pipx via pip) |
| 2b | tools |
Install CLI tools (e.g. pdm via pipx) |
| 3 | projects |
Synchronise project dependencies from lock files |
| 4 | scm |
Clone source repositories (e.g. via git) |
| 5 | post_sync |
Run post-sync commands (e.g. pdm install) |
Between Phases 2a and 2b the engine re-discovers available plugins so that
tools installed during Phase 2a (e.g. pipx) can be used as installers in
Phase 2b. This enables deferred tool resolution — a tool action whose
installer is not yet available at preview time is created with
installer=None and resolved just before execution.
Bootstrap Chain Example¶
A single manifest can bootstrap an entire toolchain from scratch:
{
"version": "1",
"runtimes": { "python": ["3.14"] },
"packages": { "python": [{ "name": "pipx" }] },
"tools": { "python": [{ "name": "pdm" }] },
"post_sync": ["pdm install"]
}
Execution order: pim/pyenv installs Python 3.14 → pip installs pipx →
re-discovery finds pipx → pipx installs pdm → pdm install syncs
project deps.
See examples/python-bootstrap/ for a ready-to-use example.
Upgrade strategy¶
All Cached Directories¶
Sync all cached directories:
Project Directory Override¶
By default, each project-sync backend auto-discovers its own project root by walking ancestor directories from the manifest's location, looking for the ecosystem's marker file:
| Ecosystem | Marker file |
|---|---|
python |
pyproject.toml |
node |
package.json |
deno |
deno.json |
This means the manifest can live in a subdirectory while the project root remains higher up:
my-project/
package.json ← Node project root (auto-discovered)
pyproject.toml ← Python project root (auto-discovered)
.porringer/
porringer.json ← manifest file
Each ecosystem independently discovers its own root, so a Python plugin and a Node plugin can resolve to different directories from the same manifest. If no marker is found, the manifest's directory is used as fallback (with a warning).
Use --project-dir to override auto-discovery for all ecosystems:
Standalone Manifests (No Project)¶
When consuming a manifest that has no associated project (e.g. a manifest
downloaded from a URL), use the API with project_directory=False to install
package-level requirements and skip project-sync backends:
params = SetupParameters(paths=manifest_path, project_directory=False)
results = api.sync.run(params)
# Inspect which actions were skipped and why
for skip in results.skips:
print(f'Skipped: {skip.action.description} — {skip.skip_reason.value}: {skip.message}')
Note: project_directory=False skips only project-sync actions (Phase 3).
Post-sync commands (Phase 4) still execute — they are independent of whether a
project directory exists. Project-sync actions are reported as skipped (not
failed), so results.success remains True. Use results.skips or
results.total_skipped to detect and act on skipped actions.
API Usage¶
import asyncio
from porringer.api import API
from porringer.schema import LocalConfiguration, SetupParameters
api = API(LocalConfiguration())
async def main():
# Full sync (project + packages)
results = await api.sync.run(SetupParameters(paths=project_path))
# Packages only — skip project backends and post-sync commands
results = await api.sync.run(SetupParameters(paths=manifest_path, project_directory=False))
# Override project directory (manifest and project in different locations)
results = await api.sync.run(SetupParameters(paths=manifest_path, project_directory=project_path))
# Execute with streaming progress
async for event in api.sync.execute_stream(SetupParameters(paths=project_path)):
print(type(event).__name__, getattr(event, 'action', None) and event.action.description or 'manifest')
asyncio.run(main())