vand
Vand materializes and pins external source instances, delegates lifecycle actions to their native tools, and records provenance independently of the source backend.
Works with any git remote — GitHub, GitLab, Bitbucket, self-hosted, file:// paths, and local folder clones — not just github.com. Git is the first source driver; the protocol is source-agnostic (source.yml manifests + origins.lock provenance ledger).
Manual CLI only — no background scheduler. For general cron/timer automation, a separate desktop scheduler project is planned later.
Why not submodules / myrepos / hawser?
| Tool | What it does | Gap |
|---|---|---|
| git submodules | Pins repos inside one repo | Detached HEADs, nested checkout pain |
myrepos (mr update) |
Update many repos | No exact SHA lockfile for replication |
hawser (haw sync) |
Multi-repo lockfile + verify | Rust stack, heavier scope |
| Repo Family | Control repo + lock | No personal catalog curation |
| vand | Catalog + origins.lock + replicate |
Small Python stdlib CLI |
Per-app library pins inside one project are separate. vand pins whole source instances across a clone root on your machine.
Three artifacts
| Artifact | Location | Job |
|---|---|---|
Manifest source.yml |
inside each source tree | Quotient map: install / update / verify / deinstall → shell commands |
Provenance ledger origins.lock |
clone root | Completed source facts: origin + pinned revision + target path (no hooks) |
| Execution log | ~/.vand/logs/ |
Attempts, timestamps, exit codes — never in source trees |
Do not confuse lock and log. A failed install must not produce a success-like ledger entry.
Install
Python 3.9+, Git, and pip. From a clone:
python -m pip install -e .
vand init --root <clone-root>
Or from a fresh clone, skip the separate pip — init runs pip install -e itself:
python vand.py init --root <clone-root>
That puts vand on PATH (Windows: the Python Scripts folder). Editable (-e) keeps the command pointed at this checkout, so self-update / git pull still work.
Without installing, you can still run python vand.py or vand.cmd from this directory.
Coding agents: follow AGENTS.md. That file is the install spec. This README is the map. First-run is python vand.py init --root <clone-root> (catalog, pip -e, skills, adopt self, examples/origins.lock). Usage skill: skills/vand/SKILL.md.
Help that machines can read
Human --help is a wall of text. Agents and other tools should not scrape it.
All machine-readable help is generated from argparse at runtime — there is no committed man page or JSON spec in the repo. If you add a flag or command, --help-json and man update automatically.
vand --help # people
vand --help-json # full spec: commands, flags, args, defaults
vand --help-json replicate
vand help --json
vand help replicate --json
vand man # roff on stdout (generated; same source as --help-json)
vand man --write FILE # optional local copy, e.g. man/vand.1 for groff
On Unix: vand man | groff -man -Tutf8 | less. Optional: vand man --write man/vand.1 then MANPATH=man man vand.
Do not commit man/ or in-repo .cursor/ skill copies — both are gitignored local output. Source of truth: vand.py (argparse) and skills/vand/SKILL.md.
Quick start
# 1. First-run (catalog + PATH + skills + adopt this clone + origins.lock)
vand init --root <clone-root>
# 2. See existing clones not yet tracked
vand scan
# 3. Register an existing folder (reads source.yml if present)
vand adopt <folder>
vand adopt <folder> --install "make install" # optional override
# 4. Check state
vand status
vand status --fetch
# 5. Export shareable ledger + human provenance log
vand export
# -> <clone-root>/origins.lock
# -> <clone-root>/VAND.md
<clone-root> is whatever directory you keep checkouts in. Catalog state lives in ~/.vand/ on this machine only.
Shared stack (Lolaplex)
This repo publishes a starter lock for the tools we share. It does not pin vand itself (the lock lives in this repo).
cd <clone-root>
git clone https://github.com/Lolaplex/vand.git
python vand/vand.py init --root .
init creates the catalog, pip install -e this clone, installs the agent skill, adopts vand, replicates examples/origins.lock, and adopts those repos. If you run init inside the vand folder with default --root ., the clone root becomes the parent.
Daily sync (pull when the other person pushed):
vand update agent-memory
vand push agent-memory # after your own commits
update fast-forwards a clean tree and re-runs install hooks when the commit changes. Dirty or diverged trees are left alone — consolidate if you both edited.
On another machine
Copy origins.lock (and optionally VAND.md), then:
vand replicate origins.lock --root <clone-root>
If you omit --root, clones land next to the lockfile. Absolute root values from another computer are ignored.
Clones missing targets, checks out exact revisions, runs each repo's install hook from its manifest.
Dry run first:
vand replicate origins.lock --root <clone-root> --dry-run
Daily workflow
vand update # fetch + fast-forward clean repos; re-run install on commit change
vand consolidate # fetch + merge when ff-only fails; lists conflicts to fix
vand consolidate --continue NAME # after fixing conflict markers
vand consolidate --abort NAME # abort stuck merge/rebase
vand push # push tracking branches; re-pin HEAD
vand pin # pin catalog to current HEAD after local commits
vand pin --export # pin + write origins.lock
vand hook-sync # once: install git hooks so plain git keeps catalog pinned
vand verify # exit 1 if any clone != ledger (CI gate); runs verify hooks
vand deinstall NAME # remove from catalog + purge target (default)
Dirty or diverged repos are never force-reset. Use consolidate when update stops at diverged/ff-only failures, then pin.
Lock-step with plain git pull / git push
update, push, pin, and consolidate write HEAD into ~/.vand/catalog.json. Your Agent, GitHub Desktop, and raw git do not.
Two different "hooks"
| Command | What it syncs | Where |
|---|---|---|
sync-hooks |
Manifest install/update shell commands from each repo's source.yml into the catalog |
catalog.json fields |
hook-sync |
Git hooks that re-pin the catalog after commit / pull / rebase / checkout | <clone>/.git/hooks/ |
Do not confuse them. sync-hooks does not install pin hooks. hook-sync does not read manifests.
Pin hooks (hook-sync)
vand hook-sync # all catalog repos
vand hook-sync agent-memory # one repo
vand pin --here # pin the catalog row for cwd (manual test)
vand pin --here --quiet # same, used inside git hooks
adopt and add run hook-sync on the new clone. Existing catalog entries: run hook-sync once after upgrading vand.
| Git hook | When it runs |
|---|---|
post-commit |
Local commit |
post-merge |
git pull that fast-forwards or merges |
post-rewrite |
Rebase / amend |
post-checkout |
Branch switch, only when HEAD actually changed |
Each hook runs vand pin --here --quiet. That looks up the catalog row by this clone's path and sets commit (and branch) to HEAD. Failures append to ~/.vand/logs/hook-pin.log and never fail the git command (|| true).
Prerequisites: clone must be in the catalog (adopt / add / init). vand must be on PATH (or the hook falls back to py -3 / python + this checkout's vand.py).
git push does not move HEAD. Commit/pull already pinned the SHA; status --fetch is enough to see whether origin is caught up.
Do not set global core.hooksPath (Git replaces per-repo hooks instead of chaining). Do not alias git. Do not auto-export origins.lock from hooks (pin --export / export stay explicit). Foreign hook files are left alone unless --force (appends the pin block after the existing script).
desktop-commander is the later scheduler clock (vand update at 09:00). Pin hooks are the residual patch for ad-hoc git in the working tree.
Remote URLs
| Form | Example |
|---|---|
| GitHub shorthand | owner/repo |
| HTTPS / SSH | https://gitlab.com/group/project.git, git@host:org/repo.git |
| Local path | a folder on disk, or file:// URL |
adopt reads origin (push target). Extra remotes are stored as mirrors only if they already contain the pinned SHA. origins.lock stores source origin + pinned revision + target. Catalog may also cache hooks and git mirrors for fetch. Read aliases: vendor.lock, vand.lock, shared.lock. v1 locks are not loaded — re-export after upgrade.
Pins are SHAs. Klix927/agent-memory and Lolaplex/agent-memory are different remotes. They are fetch sources for a pin only when that exact commit exists there. replicate / install fetch url plus listed mirrors until the lock SHA is present, then check out that SHA. update / push still follow origin. A GitHub repo name match is not identity.
For repo authors — source.yml
Stop writing install guides only in README. Add a machine-readable manifest at the repo root (version: 1 required):
# source.yml
version: 1
install: npm ci && npm run build
update: npm ci
verify: npm test
deinstall: optional prelude before vand deinstall purge
Also read via aliases: vand.yml, vand.yaml, vend.ini, json variants.
| Key | When it runs |
|---|---|
install |
After add, install, replicate |
update |
After update / consolidate when the revision changed (defaults to install) |
verify |
vand verify |
deinstall |
Optional prelude before vand deinstall purge |
Commands can be a string, a list (run in sequence with &&), or { run: scripts/setup.sh, shell: bash }.
If no manifest exists, vand tries conservative heuristics (Makefile install, package.json, requirements.txt, composer.json, go.mod, uv.lock).
vand scan # shows [manifest file] next to repos that declare hooks
vand sync-hooks # refresh catalog from on-disk manifests
The manifest is in the repo — it travels with the code and works on every machine after replicate. Hooks are not stored in origins.lock.
See source.schema.json for the JSON shape.
Where files live
| File | Purpose |
|---|---|
~/.vand/catalog.json |
Your curated repo list (private to this machine, includes local clone root) |
~/.vand/logs/ |
Timestamped logs from update/install/replicate |
~/.vand/logs/hook-pin.log |
Quiet pin failures from git hooks |
<clone>/.git/hooks/ |
Pin hooks installed by hook-sync (not in the clone's tree) |
<clone-root>/origins.lock |
Shareable provenance ledger (relative targets only) |
<clone-root>/VAND.md |
Human-readable provenance table |
<repo>/source.yml |
Quotient manifest (install/update/verify/deinstall) |
Commands
| Command | Description |
|---|---|
init [--root PATH] |
First-run: catalog, pip -e, skills, adopt self, replicate examples/origins.lock |
scan |
Git folders under root not in catalog |
add owner/repo [--install CMD] |
Clone + register |
adopt FOLDER [--install CMD] |
Register existing clone (auto-reads manifest; runs hook-sync) |
sync-hooks [NAME] |
Refresh catalog install/update commands from repo manifests (not git hooks) |
rm NAME |
Remove from catalog (keeps folder; same as deinstall --keep) |
deinstall NAME [--keep] |
Remove from catalog; default purges target directory |
status [NAME] [--fetch] |
pinned / behind / ahead / dirty / diverged / missing |
update [NAME] |
Fetch; ff-only if clean |
consolidate [NAME] [--rebase] |
Merge/rebase when update cannot ff-only |
consolidate --continue [NAME] |
Finish merge/rebase after fixing conflicts |
consolidate --abort [NAME] |
Abort in-progress merge/rebase |
push [NAME] |
Push branch |
pin [NAME] [--export] |
Pin named repo (or all) to HEAD |
pin --here [--quiet] |
Pin catalog row for cwd; --quiet for git hooks (logs on failure) |
hook-sync [NAME] [--force] |
Install git pin hooks in .git/hooks/ |
install [NAME] |
Clone missing + checkout pin + install hooks |
export [--out PATH] |
Write origins.lock + VAND.md |
replicate LOCK [--root PATH] [--dry-run] |
Bootstrap from ledger |
verify [--lock PATH] |
Drift check + verify hooks |
self-check [--fetch] [--json] |
Check if vand itself is up to date |
self-update |
Fast-forward this checkout (only refs that are descendants of HEAD) + reinstall |
install-skills |
Copy agent skill to ~/.cursor/skills and ~/.agents/skills |
help [CMD] [--json] |
Human or JSON help (JSON generated from argparse) |
man [--write FILE] |
Print / write roff man page (generated from argparse; not in repo) |
Global: --no-self-check skips the 24h residual self-update. --help-json prints the CLI spec and exits.
origins.lock format (v2)
Portable provenance only — no hooks, no branch. target is relative to the clone root you pass to replicate.
{
"version": 2,
"sources": [
{
"name": "example-app",
"source": {
"kind": "vcs",
"scheme": "git",
"origin": "https://github.com/acme/example-app.git",
"revision": "b6dfd52…"
},
"target": "example-app"
}
]
}
Automation (future)
Scheduled vand update belongs in a separate desktop scheduler project (general cron/timer for any command). vand v1 is intentionally manual with visible terminal output and logs under ~/.vand/logs/.
Example future job:
- name: sync-repos
command: vand update && vand export
cwd: /path/to/vand
schedule: "0 9 * * *"
Tests
python -m unittest discover -s tests -v
Self-update
vand keeps itself current from this checkout's remotes (personal origin + org mirror both count, same as other repos):
vand self-check # uses 24h cache when offline-friendly
vand self-check --fetch # force fresh compare
vand self-update # ff-only + `python -m pip install -e .`
After other commands, vand observes remotes at most once per 24h. If that observe shows a clean fast-forward residual, it patches (ff-only + reinstall). Within the TTL it only prints a hint from cache. Dirty or diverged trees are left alone. Disable with --no-self-check or VAND_SKIP_SELF_CHECK=1. self-update always observes and patches.
Detection order:
- If
vand.pylives in a git clone: compareHEADto configured remotes. Catalogupdatefollows origin.self-updatefast-forwards a remote tip only whenHEADis an ancestor of that tip. - Else if
originis a GitHubowner/repoURL: GitHub API - Otherwise: unknown (no hardcoded upstream)
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vand-1.0.0.tar.gz.
File metadata
- Download URL: vand-1.0.0.tar.gz
- Upload date:
- Size: 46.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
802f02d4e284c273f819b5a89190f2c8e71708d11ec6efd707e862b123939c5d
|
|
| MD5 |
d2cf55fbb55c2c9e87cf207599681de4
|
|
| BLAKE2b-256 |
6c5d4958519d50cd749fdea60328b55ff7838063cbd3ea87118b5294c9fc38ee
|
File details
Details for the file vand-1.0.0-py3-none-any.whl.
File metadata
- Download URL: vand-1.0.0-py3-none-any.whl
- Upload date:
- Size: 32.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c51ebd2c7cb5ea2ee5e8b213376817b6368aefd6253ca6451f16287bbd547f20
|
|
| MD5 |
c76813a637a888dee7084f85fde37a6f
|
|
| BLAKE2b-256 |
0269ec1d0fb3368b5885bc6f7383b1be2d2e17ff6172f8260318605dd701a078
|