25 KiB
Bread — Sync & Module System Implementation Spec
Instructions for Claude Code
This document defines exactly what to build, how it must behave, and what conditions must be met before iteration stops. Read it fully before writing any code. Do not stop iterating until every condition in the Completion Checklist at the bottom is met.
Context
Bread is a reactive desktop automation daemon for Linux. The existing codebase is a Rust workspace with three crates:
breadd/— the runtime daemon (Rust + Lua via mlua)bread-cli/— the CLI binary (Rust, talks to daemon over Unix socket IPC)bread-shared/— shared types (BreadEvent,RawEvent,AdapterSource)
The daemon exposes a Unix socket at $XDG_RUNTIME_DIR/bread/breadd.sock. The IPC protocol is newline-delimited JSON request/response. The Lua runtime runs on a dedicated OS thread. All existing code compiles and tests pass — do not break anything that currently works.
The two things being added in this iteration:
- Module system — install, list, remove, and update Lua modules from GitHub URLs
- Sync — snapshot and restore system state (Bread config + arbitrary config files + package manifests) via a Git remote
Part 1: Module System
What a module is
A Bread module is a directory (or single .lua file) that gets installed into ~/.config/bread/modules/. Modules are already loaded by the daemon — what's missing is the install/manage layer.
A module directory looks like:
~/.config/bread/modules/
└── wifi/
├── bread.module.toml ← module manifest (required)
├── init.lua ← entry point (required)
└── lib/ ← optional support files
Module manifest (bread.module.toml)
Every installed module must have a manifest:
name = "wifi"
version = "1.0.0"
description = "WiFi management for Bread"
author = "someuser"
source = "github:someuser/bread-wifi" # where it was installed from
installed_at = "2026-05-11T09:00:00Z" # RFC 3339 timestamp, set on install
All fields are required. source is the original install source string. installed_at is written by Bread at install time, not by the module author.
Install sources
The module installer must support these source formats:
github:user/repo # installs default branch
github:user/repo@v1.2.0 # installs specific tag
github:user/repo@abc1234 # installs specific commit
/path/to/local/dir # installs from local directory (copies it)
Anything else is an error with a clear message.
New Cargo dependencies allowed
Add to bread-cli/Cargo.toml as needed:
git2 = "0.18"for Git operationsreqwest = { version = "0.11", features = ["blocking", "json"] }for GitHub APIflate2,tarfor archive extraction
Add to breadd/Cargo.toml as needed:
git2 = "0.18"toml = "0.8"(already present)
CLI commands to implement
All module commands live under bread modules:
bread modules install <source> Install a module
bread modules remove <name> Remove an installed module
bread modules list List installed modules with name, version, status
bread modules update Update all installed modules to latest
bread modules update <name> Update a specific module
bread modules info <name> Show full manifest details for a module
bread modules install <source>
- Parse the source string.
- For
github:user/repo[@ref]:- Use the GitHub API to resolve the ref (or default branch if none specified).
- Download the repository archive as a
.tar.gz. - Extract to a temp directory.
- Verify a
bread.module.tomlexists at the root. If not, error cleanly. - Copy the module directory to
~/.config/bread/modules/<name>/. - Write
installed_atinto the manifest.
- For local paths:
- Verify the path exists and contains
bread.module.toml. - Copy to
~/.config/bread/modules/<name>/. - Write
installed_at.
- Verify the path exists and contains
- Print
installed <name> v<version>on success. - Tell the daemon to reload via IPC (
modules.reload) after install.
bread modules remove <name>
- Find
~/.config/bread/modules/<name>/. - Ask for confirmation:
remove <name>? (y/n). Skip if--yesflag is passed. - Delete the directory.
- Tell the daemon to reload via IPC.
- Print
removed <name>.
bread modules list
Scan ~/.config/bread/modules/ for directories containing bread.module.toml. For each, print:
wifi 1.0.0 loaded github:someuser/bread-wifi
redox 0.3.1 loaded github:breadway/bread-redox
broken-mod 0.1.0 error /home/user/local-module
Status (loaded, error, not_found, degraded) comes from the daemon's IPC modules.list response, matched by module name. If the daemon is unreachable, show unknown for status.
bread modules update [name]
- Read
bread.module.tomlfor each module to update. - If
sourcestarts withgithub:, re-run the install for that source. - If
sourceis a local path, error withcannot update local module — reinstall manually. - Print
updated <name> v<old> → v<new>or<name> already up to date.
bread modules info <name>
Print full manifest contents plus daemon-reported status. Example:
name: wifi
version: 1.0.0
description: WiFi management for Bread
author: someuser
source: github:someuser/bread-wifi
installed_at: 2026-05-11T09:00:00Z
status: loaded
Daemon-side: expose ID_VENDOR_ID and ID_MODEL_ID in udev events
In breadd/src/adapters/udev.rs, the run_udev_monitor function builds the payload for each udev event. Add vendor_id and product_id to the payload:
"vendor_id": prop_str(&event, "ID_VENDOR_ID"),
"product_id": prop_str(&event, "ID_MODEL_ID"),
These are the raw hex USB IDs (e.g. "4d44" and "5244"). Do the same in raw_change_event for the fallback poller — read them from sysfs at <syspath>/idVendor and <syspath>/idProduct if available. Also add vendor_id and product_id to the Device struct in breadd/src/core/types.rs as Option<String>.
Part 2: Sync System
Overview
Sync saves and restores a complete description of the user's environment. It is not a disk image. It saves:
- Bread config — everything in
~/.config/bread/(always included) - Delegated configs — other config directories the user explicitly opts in (e.g.
~/.config/nvim/) - Package manifest — lists of explicitly-installed packages per package manager
- Machine profile — machine name and tags for machine-aware config
Everything is stored in a Git repository. bread sync push commits and pushes. bread sync pull pulls and applies.
New crate: bread-sync
Create a new crate bread-sync/ in the workspace. Add it to [workspace.members] in the root Cargo.toml.
bread-sync/
├── Cargo.toml
└── src/
├── lib.rs
├── config.rs ← SyncConfig type, load/save
├── git.rs ← Git operations via git2
├── packages.rs ← Package manifest generation
├── delegates.rs ← Config file delegation
└── machine.rs ← Machine profile
bread-cli depends on bread-sync. breadd does not — sync is a CLI-only feature.
Sync configuration (~/.config/bread/sync.toml)
This file is created by bread sync init and edited by the user. It is committed to the sync repo.
[remote]
url = "git@github.com:user/bread-sync.git" # required, set by bread sync init
branch = "main" # default: "main"
[machine]
name = "laptop" # required, set by bread sync init
tags = ["mobile", "battery", "single-monitor"] # user-defined, optional
[packages]
enabled = true
managers = ["pacman", "pip", "npm"] # which package managers to snapshot
[delegates]
# Additional config directories to include in sync.
# ~/.config/bread/ is always included and does not need to be listed here.
include = [
"~/.config/nvim",
"~/.config/fish",
"~/.config/kitty",
]
exclude = [
"**/.git",
"**/node_modules",
"**/__pycache__",
"**/*.log",
"**/*.cache",
"~/.config/nvim/.repro",
]
All paths support ~ expansion. Globs in exclude use standard glob syntax.
Sync repo layout
The Git repository managed by Bread has this structure:
<sync-repo>/
├── bread/ ← copy of ~/.config/bread/ (minus sync.toml secrets if any)
├── configs/
│ ├── nvim/ ← copy of ~/.config/nvim/
│ ├── fish/ ← copy of ~/.config/fish/
│ └── kitty/ ← copy of ~/.config/kitty/
├── packages/
│ ├── pacman.txt ← output of `pacman -Qe`
│ ├── pip.txt ← output of `pip list --user --format=freeze`
│ └── npm.txt ← output of `npm list -g --depth=0`
├── machines/
│ └── laptop.toml ← machine profile for this machine
└── .bread-sync ← sync metadata (not committed to Git)
machines/<name>.toml contains:
name = "laptop"
hostname = "breadway-laptop" # auto-detected via gethostname
tags = ["mobile", "battery", "single-monitor"]
last_sync = "2026-05-11T09:15:00Z"
CLI commands to implement
All sync commands live under bread sync:
bread sync init [--remote <url>] Initialize sync for this machine
bread sync push [--message <msg>] Snapshot and push current state
bread sync pull Pull and apply latest state
bread sync status Show what has changed since last push
bread sync diff Show file-level diff vs remote
bread sync machines List known machines from sync repo
bread sync init [--remote <url>]
- Check if
~/.config/bread/sync.tomlalready exists. If so, error:sync already initialized. Edit ~/.config/bread/sync.toml to reconfigure. - If
--remoteis not provided, prompt:Sync remote URL (git remote or path):. - Prompt:
Machine name [laptop]:(default: hostname). - Prompt:
Machine tags (comma-separated, e.g. mobile,battery):. - Create
~/.config/bread/sync.tomlwith the provided values. - If the remote is a URL (not a local path), check if the repo exists:
- If it exists, clone it to a temp location and verify it looks like a Bread sync repo (has a
bread/directory or is empty). - If it doesn't exist, print:
remote does not exist yet — it will be created on first push.
- If it exists, clone it to a temp location and verify it looks like a Bread sync repo (has a
- Print setup summary.
bread sync push [--message <msg>]
- Load
~/.config/bread/sync.toml. Error if not initialized. - Resolve the local sync repo path (
~/.local/share/bread/sync-repo/). Clone from remote if it doesn't exist locally. - Snapshot each section:
- Copy
~/.config/bread/→<repo>/bread/(rsync-style: delete files in dest that don't exist in source) - For each path in
delegates.include: copy to<repo>/configs/<basename>/ - If
packages.enabled: run package manager queries and write to<repo>/packages/ - Write
<repo>/machines/<name>.toml
- Copy
- Stage all changes (
git add -A). - If there are no changes, print
nothing to push — already up to dateand exit. - Commit with message:
sync: <machine-name> <timestamp>or the user-provided--message. - Push to remote.
- Print a summary of what was snapshotted.
bread sync pull
- Load
~/.config/bread/sync.toml. Error if not initialized. - Pull from remote (fetch + merge or rebase — use merge, simpler).
- Apply each section in order:
- Copy
<repo>/bread/→~/.config/bread/(same rsync-style) - For each path in
delegates.includethat exists in<repo>/configs/: copy back - If
packages.enabledand--install-packagesflag is passed: run package installs (see below)
- Copy
- Tell the daemon to reload via IPC (
modules.reload) after applying. - Print a summary of what was applied.
Package install on pull (only when --install-packages is explicitly passed):
pacman.txt→sudo pacman -S --needed $(cat pacman.txt | awk '{print $1}')pip.txt→pip install --user -r pip.txtnpm.txt→ parse package names and runnpm install -g
Never run package installs automatically without the flag. Print a note at the end of pull if packages differ: run 'bread sync pull --install-packages' to install missing packages.
bread sync status
- Load sync config and local repo.
- Pull remote refs without merging (fetch only).
- Compare working tree to last commit and compare last commit to remote HEAD.
- Print:
bread sync status
machine laptop
remote git@github.com:user/bread-sync.git
last push 2026-05-11 09:15:00
local changes (not yet pushed):
M bread/init.lua
A bread/modules/wifi/init.lua
remote changes (not yet pulled):
none
bread sync diff
Run git diff HEAD in the sync repo and print it. If --remote flag is passed, run git diff HEAD..origin/<branch>.
bread sync machines
List all machines/*.toml files from the sync repo:
laptop last sync: 2026-05-11 09:15 tags: mobile, battery, single-monitor
desktop last sync: 2026-05-10 22:00 tags: stationary, multi-monitor, docked
Package manager support
Implement these four. Each must handle the case where the package manager is not installed (skip with a warning, don't error).
| Manager | Snapshot command | Install command |
|---|---|---|
pacman |
pacman -Qe |
sudo pacman -S --needed <pkg> |
pip |
pip list --user --format=freeze |
pip install --user -r <file> |
npm |
npm list -g --depth=0 --parseable |
npm install -g <pkg> |
cargo |
cargo install --list |
cargo install <pkg> |
For cargo, the snapshot format is one package per line: <name> <version>. Parse cargo install --list output accordingly.
Git operations
Use the git2 crate for all Git operations. Do not shell out to git. Required operations:
- Clone a remote repo
- Open an existing repo
- Stage all changes (
add -Aequivalent: index all tracked and untracked files) - Create a commit with a message and the current timestamp as author date
- Push to remote (support SSH and HTTPS —
git2handles this via callbacks) - Pull (fetch + merge fast-forward; if non-fast-forward, error with clear message)
- Fetch (without merging)
- Get diff between working tree and HEAD
- Get diff between HEAD and remote branch HEAD
For SSH auth, use the user's default SSH agent (git2::transport::smart::SmartSubtransport with SshKey credential). For HTTPS, use the system credential store or prompt for credentials.
Part 3: Daemon additions (IPC)
Add these IPC methods to breadd/src/ipc/mod.rs:
sync.status — returns current sync state from sync.toml if it exists:
{ "initialized": true, "machine": "laptop", "remote": "git@github.com:..." }
or { "initialized": false } if no sync.toml.
modules.install — triggers a reload after external install (already covered by modules.reload, no new method needed — bread modules install calls modules.reload via IPC after installing).
No other daemon changes are needed for sync — it is entirely CLI-side.
Part 4: Lua API additions
Add to breadd/src/lua/mod.rs in install_api:
bread.machine table:
bread.machine.name() -- returns machine name from sync.toml, or hostname if no sync.toml
bread.machine.tags() -- returns array of tags, or empty array
bread.machine.has_tag("mobile") -- returns bool
Read ~/.config/bread/sync.toml directly from Lua (parse it in Rust, expose via the API). If sync.toml doesn't exist, name() returns os.getenv("HOSTNAME") and tags() returns {}.
bread.fs table:
bread.fs.write(path, content) -- write string to file, create dirs as needed
bread.fs.read(path) -- read file to string, returns nil if not found
bread.fs.exists(path) -- returns bool
bread.fs.expand(path) -- expand ~ to home directory
All paths support ~ expansion. bread.fs.write creates parent directories automatically. Errors in write propagate as Lua errors.
Error handling requirements
Every command must handle these cases cleanly:
- Daemon not running: print
bread: daemon is not running. Start it with: systemctl --user start breaddand exit 1. - No sync.toml: print
bread: sync not initialized. Run: bread sync initand exit 1. - Network unreachable during push/pull: print the error clearly and exit 1. Do not leave the repo in a partial state.
- Module not found during remove/info: print
bread: module '<name>' is not installedand exit 1. - Git conflicts on pull: print
bread: sync conflict — resolve manually in ~/.local/share/bread/sync-repo/and exit 1. Do not auto-merge or discard changes. - Package manager not installed: warn and skip, do not fail the whole operation.
File locations
| Purpose | Path |
|---|---|
| Sync config | ~/.config/bread/sync.toml |
| Local sync repo | ~/.local/share/bread/sync-repo/ |
| Module manifests | ~/.config/bread/modules/<name>/bread.module.toml |
| Bread config | ~/.config/bread/ |
| Daemon socket | $XDG_RUNTIME_DIR/bread/breadd.sock |
All paths must use dirs crate or manual $HOME/$XDG_* expansion — never hardcode /home/breadway or any username.
Add to bread-cli/Cargo.toml: dirs = "5.0".
Tests
Module system tests (bread-cli/tests/modules.rs)
// 1. Install from local path succeeds when bread.module.toml exists
// 2. Install from local path fails when bread.module.toml is missing
// 3. Remove deletes the module directory
// 4. List reads manifests correctly from disk
// 5. Manifest is written correctly on install (all fields present, installed_at is valid RFC 3339)
Sync tests (bread-sync/tests/sync.rs)
// 1. bread sync init creates sync.toml with correct fields
// 2. bread sync push with a local bare Git repo as remote: creates correct directory structure
// 3. bread sync push snapshots bread/ directory correctly
// 4. bread sync pull copies files from repo to correct locations
// 5. Package manifest for pacman: parses output correctly
// 6. Package manifest for pip: parses output correctly
// 7. Delegates: exclude globs filter correctly
// 8. Machine profile is written to machines/<name>.toml with correct fields
// 9. Status shows no changes when working tree matches last commit
// 10. Push with no changes prints "nothing to push" and does not create a commit
All tests must pass with cargo test --workspace. Tests that require network access must be feature-gated with #[cfg(feature = "network-tests")] and not run by default.
Completion Checklist
Do not stop iterating until every item on this list is true.
Compilation
cargo build --workspacesucceeds with zero errorscargo build --workspace --releasesucceeds with zero errors- Zero compiler warnings in new code (existing warnings are acceptable)
cargo clippy --workspaceproduces no errors in new code
Tests
cargo test --workspacepasses with zero failures- All tests listed in the Tests section above exist and pass
- Integration tests in
breadd/tests/ipc_integration.rsstill pass
Module system — functional
bread modules install github:user/repodownloads and installs a modulebread modules install /local/pathcopies and installs a local modulebread modules installwith an invalid source prints a clear error and exits 1bread modules installwrites a validbread.module.tomlwith all required fields includinginstalled_atbread modules installcallsmodules.reloadIPC after successful installbread modules remove <name>removes the module directorybread modules remove <name>with--yesskips confirmationbread modules remove <nonexistent>prints a clear error and exits 1bread modules listreads all installed module manifestsbread modules listshows daemon-reported status when daemon is runningbread modules listshowsunknownstatus when daemon is not running (no crash)bread modules updatere-installs all github-sourced modulesbread modules updateskips local-path modules with a warningbread modules info <name>shows all manifest fields and daemon status
Sync — functional
bread sync initcreates~/.config/bread/sync.tomlwith all required fieldsbread sync initerrors if already initializedbread sync pushcreates the correct repo directory structurebread sync pushcopies~/.config/bread/tobread/in the repobread sync pushcopies each delegate path toconfigs/<basename>/bread sync pushwrites package manifests topackages/bread sync pushwritesmachines/<name>.tomlbread sync pushcreates a Git commit with a sensible messagebread sync pushpushes to the configured remotebread sync pushwith no changes printsnothing to pushand exits 0bread sync pullcopiesbread/from repo to~/.config/bread/bread sync pullcopiesconfigs/entries back to their original locationsbread sync pullcallsmodules.reloadIPC after applyingbread sync pull --install-packagesruns package installsbread sync pullwithout--install-packagesdoes not run package installsbread sync statusshows local uncommitted changesbread sync statusshows remote changes not yet pulledbread sync statusprintsnothing to push — already up to datewhen cleanbread sync machineslists allmachines/*.tomlentriesbread sync initwithout--remoteprompts for URL interactively
Sync — error handling
bread sync pushwithout init prints clear error and exits 1bread sync pullwithout init prints clear error and exits 1- Git conflict on pull prints clear message pointing to sync repo path and exits 1
- Package manager not installed is warned and skipped, not a fatal error
Lua API
bread.machine.name()returns machine name from sync.tomlbread.machine.name()returns hostname when sync.toml does not existbread.machine.tags()returns array of tagsbread.machine.has_tag("x")returns true/false correctlybread.fs.write(path, content)writes the file and creates parent dirsbread.fs.read(path)returns file content as stringbread.fs.read(nonexistent)returns nil, does not errorbread.fs.exists(path)returns correct boolbread.fs.expand("~/foo")returns the correct absolute path- All
bread.fspaths handle~expansion
Udev vendor/product ID
vendor_idandproduct_idfields are present in udev device eventsDevicestruct intypes.rshasvendor_id: Option<String>andproduct_id: Option<String>bread eventsoutput showsvendor_idandproduct_idwhen available
No regressions
bread reloadstill worksbread statestill worksbread eventsstill worksbread doctorstill worksbread pingstill worksbread emitstill works- Daemon starts cleanly with no existing
sync.toml - Daemon starts cleanly with a valid
sync.toml - All existing IPC methods still respond correctly
Code quality
- No hardcoded paths containing usernames or
/home/<anything> - No
unwrap()calls in new code that can fail at runtime — use?or explicit error handling - No
expect("...")calls in new async code — only in tests and truly-impossible cases - All new public functions have doc comments
bread-synccrate has aREADME.mdexplaining its purpose and public API
Implementation order
Work in this order. Do not move to the next step until the current one compiles and its tests pass.
- Add
bread-synccrate skeleton to workspace (compiles, no logic yet) - Implement
SyncConfig(load/savesync.toml) - Implement
bread sync init - Implement Git backend in
bread-sync/src/git.rs - Implement
bread sync push(bread config only, no delegates or packages yet) - Implement delegate file handling
- Implement package manifest generation
- Implement
bread sync pull - Implement
bread sync status,diff,machines - Implement
bread modules install(local path first, then GitHub) - Implement
bread modules remove,list,update,info - Add
vendor_id/product_idto udev adapter andDevicetype - Add
bread.machineLua API - Add
bread.fsLua API - Write all tests
- Run full checklist — fix anything not passing
- Run
cargo clippy --workspace— fix any new warnings