Back to projects
Developer Tooling Active 2026

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.sh links 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.sh is the one-liner for fresh remote hosts. It detects apt vs dnf/yum, installs build essentials, clones the repo, and runs install.sh --remote --languages.
  • bin/devflow is 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.