EagleKit
Project CLI with a plugin system built on Python entry points. The hard part was not the commands: it was coordinating two Git hooks that run as separate processes, share no memory, and have a human who can abort between them.
Putting [7][pagos-qa] in front of a commit looks like one line of bash. It is two processes that never overlap in time.
EagleKit is a project CLI (ek) that grows through pip-installable plugins. The Git plugins number commits per branch and mirror QA → Prod with cherry-pick. The hard part was not the commands: it was that Git spawns each hook as a new process, with no shared memory, and a human can abort between the two.
One command that does not know which subcommands it will have tomorrow
Click through the steps or let them play. The core imports no plugin: it discovers them at startup.
A whole plugin is one function and one line in pyproject.toml.
A shell wrapper, one Python process and three places where state lives
What the core does with no plugin installed.
ek add ., ek list, ek status. Workspaces to keep work, personal and client projects apart.ek add generates ~/.eagle_projects with one variable per project: cd $api. Or ek cd api through the shell function..eagle/config.yaml; .eagle/branches/<branch>/ overrides by name. String, list or script..eagle/. 15 subcommands between todo and comment.local (.git/info/exclude), repo, global or none. The recommended one touches nobody else's repo.ek plugins lists loaded and failed ones with their error. First-run wizard with ek setup.What happens inside git commit when the tracker is installed
Each hook is a new process that Git spawns and discards. This is the real flow of the public plugin, step by step.
Ana runs payments on two branches
A made-up case with three commits, marked as an example. The commands and the files are the real ones.
ek mirror create pagos creates pagos-qa and pagos-prod and links them in .git/eaglekit_mirror/links.json. ek tracker init installs the two hooks.[1][pagos-qa], [2][pagos-qa], [3][pagos-qa]. The second hook stores index, hash and UUID in branches/pagos-qa.json.ek mirror push lists the QA commits whose UUID is not in pushed_logical_ids and applies them with git cherry-pick, one at a time. Before each one it leaves a metadata file so the hook preserves the UUID.pending_commits and exits. You resolve it in the editor and continue with ek mirror push --continue: the UUID filter does not apply 1 or 2 again.sync_state.json stores the pending ones and marks in_progress. Ana resolves the conflict, runs git cherry-pick --continue, and ek mirror push --continue finishes what was left.ek mirror status: 0 pending in each direction. A hotfix made directly on Prod goes back to QA with ek mirror pull through the same mechanism.Five times the happy path was not the problem
Same pattern every time: what anyone would try first, why it does not work, what was done. The technical detail stays folded.
Designing the happy path took a day. Making sure nothing breaks when someone aborts a commit halfway, amends, or the fifth of five cherry-picks conflicts, took weeks. The complexity of a CLI is not in the commands: it is in the resilience.
Where each thing goes, and why
Almost all of them are the same question: does someone who only wants ek add . and ek list need this?
cd in the shell that launched it. The ek() function intercepts ek cd api, asks for the path with ek-core cd --path api and runs the real cd. Everything else passes straight through to Python. It is the same pattern nvm, pyenv and direnv use.Go deeper
ek shell install adds the function between markers in .zshrc or .bashrc; uninstall removes only that block. ek add also regenerates ~/.eagle_projects with export api="/path" per project, so cd $api works without the function.
router.py that translates ek deploy into ek-core run task deploy, and the README documents it, but in v0.2.1 the ek entry point points at the wrapper, not the router. Today the shortcut is not wired: the real form is ek run task deploy.pipx inject eaglekit eaglekit-plugin-mirror and the command exists; the plugin's dependencies are resolved by packaging, not by the CLI. A ~/.eaglekit/plugins/ folder would mean inventing a format, versioning it and resolving imports by hand.Go deeper
The contract is deliberately minimal: one group, eaglekit.plugins, and one function, register(app). The core knows no plugin by name and there is no central list to maintain. The cost is challenge 05: each plugin picks its own namespace.
Go deeper
Each plugin can be installed on its own: it declares eaglekit as a dependency and pulls in the core if it is missing. The two public plugins follow Clean Architecture with domain, application, infrastructure and presentation layers: the mirror's ARCHITECTURE.md.
flow.db in .git/eaglekit/: WAL mode for writes from hooks, indexed SQL for queries and one transaction per cherry-pick. SQLite ships with Python: no server, no new dependency.Go deeper
Five decisions I would take the other way today
Recap
ek command that does not know its plugins: it discovers them through entry points at startup, each in its own namespace, and a broken plugin only produces a warning.