Repositories
Repositories tell Peppy where to look for nodes, launchers, contracts, and pairings. Each repository states what it publishes through a committed peppy_repository.json5 at its root; peppy repo refresh reads that index from every configured repository and caches the result. The index itself is written by peppy repo index, which walks the tree and records:
- Nodes, identified by content: every
.json5file whose body declarespeppy_schema: "node/v1"is treated as a node config, regardless of its filename, keyed by thename:tagdeclared in its manifest. - Launchers, identified by content: every
.json5file whose body declarespeppy_schema: "launcher/v1"is treated as a launcher, regardless of its filename. The launcher is keyed by the file stem (e.g.openarm01_sim_teleop.json5becomes the launcher namedopenarm01_sim_teleop). - Contracts, identified by content: every
.json5file whose body declarespeppy_schema: "contract/v1"is treated as a contract, regardless of its filename. A contract is a reusable declaration of topics, services, and actions, keyed by thename:tagdeclared in its manifest; nodes claim contracts byname:taginmanifest.implements. - Pairings, identified by content: every
.json5file whose body declarespeppy_schema: "pairing/v1"is treated as a pairing, regardless of its filename. A pairing is a two-role, topics-only contract that two node instances pair 1:1 over, keyed by thename:tagdeclared in its manifest; see Pairing.
Out of the box, four repositories are configured:
nodes-hub(https://github.com/Peppy-bot/nodes-hub.git, tracked onmain, id1000): a curated collection of ready-to-use nodes.launchers-hub(https://github.com/Peppy-bot/launchers-hub.git, tracked onmain, id1001): community launch files that compose nodes from the hubs.openarm-nodes(https://github.com/Peppy-bot/openarm-nodes.git, tracked onmain, id1002): nodes specific to the OpenArm01 robot.contracts-hub(https://github.com/Peppy-bot/contracts-hub.git, tracked onmain, id1003): shared contract definitions that nodes claim byname:taginmanifest.implements.
The id is the priority key when several repositories provide the same name:tag, so the defaults resolve in this order.
Add your own local directory with peppy repo add /path/to/my/nodes, then peppy repo index /path/to/my/nodes and peppy repo refresh, so Peppy can discover nodes you create locally. A repository publishes through its committed index, so all three steps are needed before a local node resolves.
Configuration files
Section titled “Configuration files”Repository configuration lives in ~/.peppy/conf/, and the indexes built by peppy repo refresh are written to ~/.peppy/cache/:
| File | Purpose |
|---|---|
conf/repositories.json5 | Repositories to scan |
conf/excluded_repositories.json5 | Repositories (or subdirectories) to skip |
cache/nodes.json5 | Index of nodes discovered across repositories |
cache/launchers.json5 | Index of launch files discovered across repositories |
cache/contracts.json5 | Index of contract definitions discovered across repositories |
cache/pairings.json5 | Index of pairing definitions discovered across repositories |
cache/repo_status.json5 | Per-repository read status: when each one was last read successfully, and how its most recent read failed |
The two conf/ files are JSON5 arrays. Each entry has an id (auto-assigned if missing), a type, and source-specific fields:
[ { id: 1, type: "fs", path: "/home/user" }, { id: 2, type: "git", url: "https://github.com/Peppy-bot/nodes-hub.git", ref: "main" },]Source types
Section titled “Source types”| Type | Fields | Description |
|---|---|---|
fs | path | A local directory, read through its committed peppy_repository.json5. |
git | url, ref (optional) | A git repository. Peppy shallow-clones it and reads the same committed index. Use ref to pin a branch, tag, or commit. |
These are the only two source types. An entry with any other type fails its read as unreachable with an “unrecognized repository entry” detail.
An fs repository is a path only this machine can read, so a deployment resolved from one must keep
all of its instances on this daemon in a federated launch; serve the nodes from a git repository
to place them on other machines (see Federation limits).
Commands
Section titled “Commands”Initialize the defaults
Section titled “Initialize the defaults”peppy repo initSyncs repositories.json5 with the bundled default template. If the file does not yet exist it is created verbatim; otherwise any missing default entries are appended without touching your existing entries. Use this after upgrading Peppy to pick up new defaults without having to restart the daemon. The command operates directly on the local config file; no daemon connection is required.
List repositories
Section titled “List repositories”peppy repo listShows the indexed nodes grouped by the repository that provides them. Each group is headed by the repository’s display label (path for fs, url (ref: r) for git) followed by the node count and source kind, then lists each node’s name, tag, and path.
The list reflects the index, not the filesystem, so it shows what will actually resolve at launch. A node added to a local repository does not appear until you run peppy repo index <path> and then peppy repo refresh, which is also when it becomes launchable.
One situation is flagged: (shadowed by <repository>) means another repository with a lower id provides the same name:tag. This resolves deterministically to the named repository; the shadowed entry is recorded and inspectable but not used. A supported arrangement, not a problem.
A repository whose last refresh failed is marked [retained, last read <date>], with the reason on the following line. Its entries are the ones it last published successfully, so anything referencing them keeps working; the date tells you how old they are.
Refresh the index
Section titled “Refresh the index”peppy repo refreshRe-scans all configured repositories and rebuilds the node, launcher, contract, and pairing indexes. peppy repo update is accepted as an alias. During refresh, Peppy:
- Reads
repositories.json5(creates it with defaults on first run). - Skips any repository or path listed in
excluded_repositories.json5. - Reads local directories in place and shallow-clones git repositories.
- Records what each repository’s committed
peppy_repository.json5publishes: nodes and their tags, launchers (one per name, no tag), and contracts and pairings (keyed by their manifestname:tag). A repository without that file fails its read with instructions to runpeppy repo index. - Reports each discovered node, launcher, contract, pairing, and excluded repository in real time.
- Caches results in
~/.peppy/cache/nodes.json5,~/.peppy/cache/launchers.json5,~/.peppy/cache/contracts.json5, and~/.peppy/cache/pairings.json5, and records per-repository read status in~/.peppy/cache/repo_status.json5.
On completion it prints a summary, for example Repository refresh complete. 12 node(s), 3 launcher(s), 5 contract(s), 2 pairing(s) found.
When multiple repositories provide the same name:tag pair, the repository with the lower id takes priority. The shadowed entry is still recorded and shown in repo list but does not override the primary source.
When one repository provides the same name:tag twice, there is no winner to pick. That repository fails its refresh; see When a repository fails below.
When a repository fails
Section titled “When a repository fails”A failure is scoped to the repository that caused it. The repositories that read cleanly are indexed as usual, so a problem in one hub never blocks picking up changes from the others.
A repository that fails keeps the entries it last published. Nothing disappears out from under a launcher that references it, and the machine keeps launching everything it could launch before. peppy repo list marks those entries as retained, with the date they were read.
The run itself still succeeds. A repository that could not be read is a state to report, not a reason to stop: everything the run could do, it did. It names every repository that failed in one message, so you fix everything after one run rather than running repeatedly, and ends by pointing you back at the retry:
Repository refresh complete. 26 node(s), 4 launcher(s), 8 contract(s), 2 pairing(s) found.2 of the configured repositories could not be updated. Every other repository was updated normally. - repository 1000 (https://github.com/Peppy-bot/nodes-hub.git (ref: main)) contradicts itself [conflict]: duplicate key `v1`: already declared at uvc_camera_video_reconstruction/python/peppy.json5, declared again at uvc_camera_video_reconstruction/rust/peppy.json5. Kept 18 entries from its last successful read - repository 1003 (https://github.com/Peppy-bot/contracts-hub.git (ref: main)) could not be read [unreachable]: failed to connect to github.com. Kept 8 entries from its last successful readRun `peppy repo refresh` again to pick those repositories up.This is also what lets the installer finish on a machine whose network is flaky. Its last step is a refresh, so a hub that happens to be unreachable used to end an install that had otherwise worked. Now the install completes, tells you which repositories it could not read, and leaves you the same retry.
Only a refresh that could not run at all fails: a repositories.json5 that does not parse or that lists the same id twice, or caches that cannot be written. Nothing was read in that case, so there is nothing to contain.
Two kinds of failure are reported, and the distinction matters because they send you to different places:
| Kind | Meaning | Typical cause |
|---|---|---|
unreachable | The repository could not be read at all. | Network outage, a bad git ref, a local path that is not mounted, a root with no committed peppy_repository.json5, an unrecognized entry in repositories.json5. |
conflict | The repository was read fine, but its committed index contradicts itself or the tree. | An index that fails to parse (a duplicate key, an unknown field, an illegal path), or a listed path that is missing, is of another kind, or declares another identity. |
An unreachable repository is an outage, so the fix is usually to wait or fix connectivity. A conflict is a content bug in the repository, and the message names the exact files, so the fix is a change to that repository.
The report is ordered by repository id and never inherits the filesystem’s directory order, so two machines seeing the same problem report it identically.
Add a repository
Section titled “Add a repository”peppy repo add <source> [--ref <tag-or-branch>] [--top]Adds a new repository to repositories.json5. The source format is auto-detected:
# Local directorypeppy repo add /path/to/my/nodes
# Git repositorypeppy repo add https://github.com/org/repo.git
# Git repository pinned to a branch or tagpeppy repo add https://github.com/org/repo.git --ref v2.0
# Give the new repo top priority (lower `id` than every existing entry)peppy repo add /path/to/my/nodes --topThe --ref flag is only valid for git sources. By default the new repository is appended with id = max(existing ids) + 1, so it has the lowest priority among configured repositories; pass --top to assign it an id just below the current minimum, giving it the highest priority. The priority id decides which repository wins when several of them provide the same name:tag (see Refresh the index).
Remove a repository
Section titled “Remove a repository”peppy repo remove <id>Removes a repository by its numeric ID (shown by peppy repo list). The repository index is automatically refreshed after removal.
Exclude a repository
Section titled “Exclude a repository”peppy repo exclude <source> [--ref <tag-or-branch>]Adds a source to excluded_repositories.json5. Excluded repositories are skipped during peppy repo refresh, and the repository index is automatically refreshed after the exclusion. You can exclude an entire repository or a specific subdirectory within a local repository:
# Exclude a whole git repositorypeppy repo exclude https://github.com/org/repo.git
# Exclude a subdirectory of a local repositorypeppy repo exclude /home/user/projects/private-nodesThe repository index
Section titled “The repository index”A repository index is a peppy_repository.json5 at the root of the tree Peppy scans, stating what
the repository publishes and where each item is declared. peppy repo index
writes it and --check verifies it against the tree, and it is committed alongside the items it
lists, so a contested identity is caught on the branch that created it rather than on every machine
that later refreshes.
The nesting is the uniqueness rule: name, then tag, then the file that declares the item. A second claim on one identity has nowhere to go except a key that is already taken, so a repository cannot state two answers to the same question.
// peppy_repository.json5, at the root of the tree Peppy scans{ peppy_schema: "repository/v1",
nodes: { uvc_camera: { v1: { path: "uvc_camera/rust/peppy.json5" }, v2: { path: "uvc_camera/python/peppy.json5" }, }, }, contracts: { rgb_camera: { v1: { path: "cameras/rgb_camera.json5" } } }, pairings: { joint_link: { v1: { path: "robot/joint_link.json5" } } },
// One level deep: a launcher is identified by its file stem and carries no tag. launchers: { openarm_v1_teleop: { path: "openarm/openarm_v1_teleop.json5" } },}Rules worth knowing before you hand-edit one:
pathnames the declaring file, not a directory. One rule across all four kinds, since only nodes are directory-shaped.- A repeated key is an error, not a last-one-wins overwrite. The message names the identity and both files.
- Paths stay inside the repository. Absolute paths,
..,., repeated separators, backslashes and trailing separators are all refused, as is a path that reaches its target through a symlink out of the tree. - Only the mapping belongs here. Nothing is copied out of an item’s own manifest, because a copy would drift from the thing it copied.
- Unknown fields and unknown versions are refused rather than ignored, so an index Peppy does not fully understand is rejected outright instead of silently losing part of itself.
- An empty index is valid; a missing one is not. A repository that publishes nothing has an index
with no sections, and
--checkfails on a repository that carries no index at all. “Publishes nothing” and “has no index” are different situations and get different answers. - The file is generated. Comments survive
--check, but the nextpeppy repo indexrun discards them.
Index a repository
Section titled “Index a repository”peppy repo index [PATH] # write PATH/peppy_repository.json5 (PATH defaults to .)peppy repo index [PATH] --check # verify the committed index instead of writing itNeither form needs a running daemon, so both work in CI and on a branch before anything is merged.
Writing walks the repository and records what it finds. If one identity is claimed twice, generation refuses and names both files:
2 node manifests claim `uvc_camera_video_reconstruction:v1`: uvc_camera_video_reconstruction/python/peppy.json5, uvc_camera_video_reconstruction/rust/peppy.json5--check verifies four things against the repository, and reports everything it finds at once:
- every item in the tree is listed (the mistake people actually make is adding an item and forgetting the index, which would otherwise leave it silently invisible);
- every listed path is a real file inside the repository;
- every listed path declares exactly the kind and identity it is filed under;
- every listed path is where the identity is actually declared: an item whose declaring file moved is reported as moved, not as missing.
Run it on every pull request to a repository others consume. Catching a contested identity on the branch of the person who caused it is the entire point: the alternative is every machine in the fleet reporting it the next morning, none of which can fix it.
The remedy for any failure is the same: re-run peppy repo index and commit the result.
Using a repository-indexed node
Section titled “Using a repository-indexed node”Once a node appears in peppy repo list, you can add it by its name:tag without supplying a path or URL; Peppy resolves the source through the cached index at ~/.peppy/cache/nodes.json5:
peppy node add uvc_camera:v1This is the shortest form of peppy node add. It works for any node provided by any repository listed in repositories.json5, including the default nodes-hub community repository. When several repositories provide the same name:tag, the resolution rule from repo refresh applies: the repository with the lower id wins.
Resolving a name:tag has three outcomes, not two:
-
Resolved. Exactly one repository wins, either because it is the only one providing the identity or because it has the lowest
id. -
Not found. No repository provides it. The error names the cache file it looked in, and if you have excluded any repositories it says so, since an excluded repository is never indexed and may well have been the one that provided it.
-
Ambiguous. The cache states that the winning repository claims the identity more than once. A generated index cannot express this (each identity is a key that exists once), so hitting it means the cache file was edited by hand. It is a distinct error naming both files, never a “not found”, since reporting it as missing would send you to add a node that is already there twice:
2 node entries in repository 1000 claim `uvc_camera_video_reconstruction:v1`:uvc_camera_video_reconstruction/python/peppy.json5,uvc_camera_video_reconstruction/rust/peppy.json5;fix the repository so one manifest claims it, then run `peppy repo refresh`
One constraint applies to this source shape:
--refis rejected. The git ref (if any) is pinned once inrepositories.json5when you register the repo, not per-add.
If the node you want to add lives in a repository that is not in repositories.json5, keep using the full git URL or HTTP archive form shown in Sharing nodes.
A manifest can also pin the exact content of a contract or pairing document it references with a sha256 fingerprint, independent of which repository provides it; see Content pins.
Syncing against repositories
Section titled “Syncing against repositories”peppy node sync regenerates a node’s interface code (peppygen) from its peppy.json5. By default, every dependency the node declares must already be in the node stack, otherwise the sync fails with an “X does not exist in the stack” error.
Pass -r (or --include-repositories) to let Peppy fall back to the repository cache when a dependency is missing from the stack:
peppy node sync -rThe lookup order is:
- Node stack: wins whenever the dependency is already in the stack. Stack-resolved deps are listed under
Synchronized from node stack:in the command output. - Repository cache (
~/.peppy/cache/nodes.json5): consulted only when the stack does not have the dependency. Repo-resolved deps are listed underSynchronized from repositories:, each tagged with its source kind (fsorgit).
Repository-resolved git dependencies reuse the same persistent checkout cache as peppy node add, so the same repository is never cloned more than once during a single sync run.
Example output when both layers contribute:
Syncing node from /workspace/my_robot_brain via daemon 'cn-...'Synced node interfaces at /workspace/my_robot_brainSynchronized from node stack: - already_added_dep:v1Synchronized from repositories: - uvc_camera:v1 (git) - lidar_sensor:v1 (fs)A dependency that is missing from both the node stack and every configured repository is a hard failure:
dep `gps_module:v2` not found in node stack or repository cache; run `peppy repo refresh`peppy node add applies the same stack-then-cache lookup automatically, without a flag, but only for the direct dependencies of empty-admitting slots (cardinality: "zero_or_one" or "zero_or_more") that are absent from the stack: their interfaces still feed code generation, each cache resolution is reported in the add output, and a dependency missing from both sources refuses the add with the same error. A dependency whose slot demands an instance must still be in the stack before the add.
Tip: register your local workspace as a repository
Section titled “Tip: register your local workspace as a repository”When a node depends on another node you maintain locally, registering your workspace directory as a fs repository removes the need to peppy node add every dependency just to regenerate peppygen for a downstream node:
peppy repo add ~/code/my-nodespeppy repo index ~/code/my-nodes # writes peppy_repository.json5, stating what the directory publishespeppy repo refreshpeppy node sync -r # picks up uvc_camera, lidar_sensor, ... from ~/code/my-nodesRe-run peppy repo index ~/code/my-nodes and peppy repo refresh after adding, moving, or renaming
a node in that directory: the refresh reads the committed index, and repo list and node sync -r
both read the cache, not the filesystem.
Particularly useful during the early phase of a multi-node project, when the dependency graph is still in flux and you don’t want to re-add nodes after every interface change.
Directory pruning
Section titled “Directory pruning”When peppy repo index walks a repository to write its index, it skips hidden files and directories
(any name with a leading .) plus the following directories:
.git.peppytargetnode_modules.venv__pycache__
A repository’s own .gitignore and .ignore files are honoured too, while a contributor’s global
gitignore and the clone’s .git/info/exclude are not read. peppy repo refresh does not walk the
tree at all: it resolves exactly the paths the committed index names.