Skip to main content

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:

  1. If vand.py lives in a git clone: compare HEAD to configured remotes. Catalog update follows origin. self-update fast-forwards a remote tip only when HEAD is an ancestor of that tip.
  2. Else if origin is a GitHub owner/repo URL: GitHub API
  3. 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

vand-1.0.0.tar.gz (46.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vand-1.0.0-py3-none-any.whl (32.6 kB view details)

Uploaded Python 3

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

Hashes for vand-1.0.0.tar.gz
Algorithm Hash digest
SHA256 802f02d4e284c273f819b5a89190f2c8e71708d11ec6efd707e862b123939c5d
MD5 d2cf55fbb55c2c9e87cf207599681de4
BLAKE2b-256 6c5d4958519d50cd749fdea60328b55ff7838063cbd3ea87118b5294c9fc38ee

See more details on using hashes here.

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

Hashes for vand-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c51ebd2c7cb5ea2ee5e8b213376817b6368aefd6253ca6451f16287bbd547f20
MD5 c76813a637a888dee7084f85fde37a6f
BLAKE2b-256 0269ec1d0fb3368b5885bc6f7383b1be2d2e17ff6172f8260318605dd701a078

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page