expri
expri is a repo-local remote workflow tool. The first implemented command is
sync, which makes a remote working tree match local HEAD plus dirty and
untracked local files.
Sync
Top-level commands are controller-side commands: they run from your workstation
and operate on a configured target. expri node ... is the target-machine
namespace for commands that run locally on a synced node.
Create an expri.toml in the repo you want to sync, and keep machine targets
in a sibling target file. The target filename follows the config filename:
expri.toml uses expri.target.toml, and cs336.toml uses
cs336.target.toml. Target files are local/private; add them to that repo's
.gitignore.
# expri.toml
[]
= "my-project"
[]
= ["uv.lock"]
[]
= "wandb"
[]
= ["pnpm", "dev"]
= { = ["python", "scripts/train.py"], = true }
# expri.target.toml
[]
= "user@example.com"
= "~/my-project"
= "auto"
= "expri"
Then run:
See examples/cs336.toml for a CS336-shaped starting point.
Targets default to protocol = "auto", which tries expri node sync-apply
first and falls back to the SSH protocol. Set protocol = "expri-node" to
require the node binary, or protocol = "ssh" for the fallback path.
Task
expri run <name> runs a repo-local command alias in the repo root:
[]
= ["pnpm", "dev"]
= { = ["python", "scripts/train.py"], = true }
The array form runs exactly that command. The object form supports options;
uv = true prefixes the command with uv run.
When -T/--target is provided, run syncs the repo to the target before
executing the task there. Pass --no-sync before the task name to skip that
sync. Run options must appear before the task name; arguments after the task
name are passed through to the task.
Setup
expri -T <target> setup runs repo-configured setup steps on the target. Built-in
steps are uv, hf, and script; scripts are resolved relative to the remote
repo root.
or from inside that repo:
The sync algorithm uploads committed history with a git bundle, stages HEAD
plus a zip archive of local dirty and untracked files, then installs the staged
files on the remote. It removes previously synced files absent from the staged
tree and preserves unrelated remote-generated files. Remote tool state lives
under .expri/.
Use sync.remote_managed for repo-relative files that the target should own,
even if they are tracked by Git or appear in the dirty patch. For example,
remote_managed = ["uv.lock"] preserves the target's lockfile across syncs and
excludes local changes to that file from patch.zip.
For a path-scoped rsync, pass paths after --. Only files returned by
git ls-files under those paths are transferred:
Download
expri -T <target> download downloads configured result mappings into
results/<target>/. Mappings are declared in expri.toml:
[]
= ["*.pt"]
[]
= "wandb"
= "out/jobs"
That example downloads the remote repo's wandb/ directory into
results/<target>/wandb/, and out/jobs/ into results/<target>/jobs/.
Ignore patterns are passed to rsync as excludes relative to each mapping root.
Pass mapping names after -- to download a subset:
Use --dry-run to print the SSH/rsync commands without executing them.