Devflow
My dev workflow as code. A dotfiles repo, a bootstrap script, and a small CLI that bring any new mac or Linux box up to a known-good state.
Workflow as code
- TypeScript
- Shell
- Git
- Automation
- CLI
Why I built this
Every new machine, every fresh VPS, every time I had to re-image my laptop, the same hour or two evaporated. Install zsh. Find my prompt config. Get the right node version manager. Set up tmux, nvim, copy across some shell aliases I half-remembered. Every environment ended up slightly different from the last.
I wanted one repo I could clone (or pipe through bash once on
a fresh Linux box) and have the same shell, same editor, same
key bindings, and same language toolchain on every host I
touch. Not a framework, just my workflow written down in a way I
can run.
What it is
A dotfiles repo, plus a bootstrap script, plus a small CLI on top:
install.shlinks configs, installs base packages, and sets the login shell. Re-running is safe: existing dotfiles get backed up to*.backup.devflow.<timestamp>first.scripts/bootstrap.shis the one-liner for fresh remote hosts. It detects apt vs dnf/yum, installs build essentials, clones the repo, and runsinstall.sh --remote --languages.bin/devflowis the small CLI: tmux sessions, healthcheck, PATH debug, self-update.
# fresh remote linux host
curl -fsSL https://raw.githubusercontent.com/uzairali19/devflow/main/scripts/bootstrap.sh | bash
# local
git clone https://github.com/uzairali19/devflow.git ~/devflow
cd ~/devflow && ./install.sh --local --languages
DEVFLOW_DIR, DEVFLOW_BRANCH, and DEVFLOW_REPO_URL env vars
override the install location, branch, and remote. That way the
same bootstrap works against forks without code changes.
What’s in the box
zsh, starship, tmux, nvim (Lazy + Kickstart-style layout), git, fzf, ripgrep, fd, bat, eza, jq, tree, and mise. Ghostty config gets installed on macOS only.
mise replaces the whole nvm/pyenv/rbenv/asdf stack: one
binary, one shim layer, one global config (node lts,
python 3.12, go latest, rust stable) with project-local
overrides via .mise.toml. The zshrc only loads nvm or pyenv as
legacy fallbacks if mise isn’t installed, so cold zsh startup
stays fast.
What gets linked
~/.zshrc -> configs/zsh/zshrc
~/.tmux.conf -> configs/tmux/tmux.conf
~/.config/starship.toml -> configs/starship/starship.toml
~/.config/nvim -> configs/nvim/
~/.config/mise/config.toml -> configs/mise/config.toml
~/.config/ghostty -> configs/ghostty/ (macOS only)
~/.local/bin/devflow -> bin/devflow
~/.zshrc.local is copied from the example once and then left
alone on subsequent runs. That’s where machine-specific exports
and API keys go. It’s git-ignored, and sourced last so it can
override anything in the committed zshrc. Secrets never live in
the repo.
The CLI
devflow session [name] new or attach tmux session
devflow sessions list sessions
devflow doctor run healthcheck
devflow debug dump PATH and version-manager state
devflow update git pull + re-link configs
devflow path print repo root
devflow session is the one I use most. With no argument it
slugs the current directory’s basename and creates or attaches
a tmux session named after it. Run from inside tmux, it
switches to the matching session. Outside, it execs
tmux new-session -A.
devflow doctor runs a healthcheck: required tools on PATH,
mise installed, configs symlinked to the right places, login
shell set. Then it prints a green/red summary. Useful after
bootstrapping a new host, or for chasing “why does this only
break here?”
devflow update is the upgrade path. It runs
git pull --ff-only in the repo and then link-configs.sh, so
any new configs from the update get wired up.
Bootstrap flow
Each script is small and idempotent. link-configs.sh removes
a symlink before re-creating it, the package installers skip
what’s already there, and mise install only fetches versions
that aren’t already present. Running the whole thing twice in
a row should be a no-op.
Decisions worth calling out
No oh-my-zsh. No plugin manager. Starship for the prompt, a small zshrc, and that’s it. Keeping cold zsh start time short is something I actually care about.
mise over nvm/pyenv/asdf. One global config, one binary, one shim layer. The nvm/pyenv fallbacks only stay because some hosts already have them installed and removing them would break other projects on the same box.
.zshrc.local for secrets. Anything host-specific or
sensitive lives there. The committed zshrc never sources from
.env files in the repo, and zshrc.local.example is the only
file copied (not symlinked) so it’s safe to edit per host.
Symlinks, with backups. Re-running the installer overwrites
links, never files. Anything it would otherwise clobber gets
renamed to *.backup.devflow.<timestamp> first. There’s no
uninstaller: remove the symlinks, restore from the backups,
chsh back. That constraint is what kept the install logic
small.
One CLI command for the workflow loop. devflow session is
the single entry point I use to start working on something.
Everything else (opening the editor, running servers, attaching
to a host) happens inside that tmux session.
What I learned
The hard part isn’t choosing tools. It’s writing the install
scripts so they’re safe to run again. Most of the bugs lived in
edge cases: a config file that was a real file instead of a
broken symlink, an old .zshrc getting clobbered before the
backup ran, a chsh failing because zsh wasn’t in
/etc/shells yet.
Once the bootstrap was idempotent, every other change became
cheap. Add a tool, push, run devflow update everywhere, done.
The other thing I learned is that “personal” doesn’t mean “one
host.” Because the bootstrap takes DEVFLOW_DIR,
DEVFLOW_BRANCH, and DEVFLOW_REPO_URL, I can fork it for
someone, point the one-liner at the fork, and they get a clean
install of their own version. The workflow lives in code, not
in a wiki page.
Source on GitHub.