← All articles
PRODUCTIVITY Developer Productivity Toolchain: Raycast, direnv, m... 2026-02-09 · 8 min read · productivity · workflow · automation

Developer Productivity Toolchain: Raycast, direnv, mise, just, and nushell

Productivity 2026-02-09 · 8 min read productivity workflow automation developer-tools terminal

Developer Productivity Toolchain: Raycast, direnv, mise, just, and nushell

The difference between a fast developer and a slow one is rarely about typing speed or algorithmic knowledge. It is about the toolchain -- the dozen small tools that save 5 seconds each, hundreds of times a day. A launcher that opens the right project in two keystrokes. An environment manager that activates the right Node version when you enter a directory. A task runner that replaces a wall of Makefile syntax with readable commands.

This guide covers tools that compound. Each one saves a small amount of time, but together they transform how you interact with your machine.

Raycast / Alfred: Application Launchers on Steroids

Application launchers are the entry point to your workflow. Raycast (macOS, free) and Alfred (macOS, paid for advanced features) replace Spotlight with something programmable. On Linux, Ulauncher and Albert fill the same role.

Raycast Configuration

Raycast's power comes from extensions and custom scripts.

# Install Raycast extensions via the store:
# - GitHub: search repos, PRs, issues without opening a browser
# - Docker: manage containers from the launcher
# - Jira/Linear: create and search tickets
# - Clipboard History: searchable clipboard with pinned items
# - Snippets: text expansion (e.g., "@@email" → your email)

Custom Raycast Script Commands

#!/bin/bash
# ~/.config/raycast/scripts/new-branch.sh

# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title New Git Branch
# @raycast.mode compact
# @raycast.argument1 { "type": "text", "placeholder": "branch name" }
# @raycast.packageName Developer

cd ~/projects/current || exit 1
git checkout -b "feature/$1"
echo "Created branch feature/$1"
#!/bin/bash
# ~/.config/raycast/scripts/docker-restart.sh

# @raycast.schemaVersion 1
# @raycast.title Restart Docker Compose
# @raycast.mode compact
# @raycast.argument1 { "type": "text", "placeholder": "service name" }
# @raycast.packageName Docker

cd ~/projects/current || exit 1
docker compose restart "$1"
echo "Restarted $1"

Linux Alternative: Ulauncher

// ~/.config/ulauncher/shortcuts.json
{
  "shortcuts": [
    {
      "keyword": "gh",
      "cmd": "xdg-open https://github.com/search?q=%s",
      "name": "GitHub Search"
    },
    {
      "keyword": "docs",
      "cmd": "xdg-open https://devdocs.io/#q=%s",
      "name": "DevDocs Search"
    }
  ]
}

direnv: Automatic Environment Per Directory

direnv loads and unloads environment variables when you enter and leave a directory. No more running source .env manually, no more accidentally using production credentials in development.

Setup

# Install
brew install direnv  # macOS
sudo apt install direnv  # Debian/Ubuntu
sudo dnf install direnv  # Fedora

# Add to shell (add to ~/.bashrc or ~/.zshrc)
eval "$(direnv hook bash)"
# or
eval "$(direnv hook zsh)"

Usage

# In your project directory, create a .envrc file
# .envrc
export DATABASE_URL="postgres://localhost:5432/myapp_dev"
export REDIS_URL="redis://localhost:6379"
export AWS_PROFILE="myapp-dev"
export NODE_ENV="development"

# Allow the file (required for security -- direnv won't run untrusted .envrc)
direnv allow

# Now when you cd into this directory, these vars are set
# When you cd out, they're unset

Advanced: direnv Stdlib

direnv includes a standard library of helper functions.

# .envrc

# Load a .env file (Docker-compatible format)
dotenv

# Use a specific Node.js version (via nvm)
use node 20

# Use a specific Python version (via pyenv)
use python 3.12

# Add local bin to PATH (useful for project-specific tools)
PATH_add bin
PATH_add node_modules/.bin

# Source another envrc (for shared team configuration)
source_env ../.envrc.shared

# Load secrets from a file outside the repo
source_env ~/.secrets/myapp.env

# Set layout for language-specific conventions
layout python    # Creates and activates a virtualenv
layout node      # Adds node_modules/.bin to PATH
layout ruby      # Sets up GEM_HOME

Team Configuration

# .envrc.template (committed to git)
# Copy to .envrc and fill in values
export DATABASE_URL="postgres://localhost:5432/myapp_dev"
export API_KEY=""  # Get from 1Password vault
export STRIPE_KEY=""  # Get from Stripe dashboard (test mode)

# .gitignore
.envrc
!.envrc.template

mise (formerly rtx): Universal Version Manager

mise replaces nvm, pyenv, rbenv, and every other language-specific version manager with a single tool. It is fast (written in Rust), supports every language, and integrates with direnv.

Setup

# Install
curl https://mise.run | sh

# Add to shell
echo 'eval "$(mise activate bash)"' >> ~/.bashrc
# or
echo 'eval "$(mise activate zsh)"' >> ~/.zshrc

Usage

# Install tools
mise use node@20       # Install and set Node 20 for this directory
mise use [email protected]   # Install and set Python 3.12
mise use [email protected]       # Install Go 1.22
mise use bun@latest    # Install latest Bun
mise use [email protected] # Even non-language tools

# Global defaults
mise use --global node@20
mise use --global [email protected]

# List installed versions
mise list

# Project-specific versions are stored in .mise.toml

Configuration

# .mise.toml (committed to git)
[tools]
node = "20"
python = "3.12"
bun = "latest"
terraform = "1.7"

[env]
NODE_ENV = "development"

# Run tasks (mise also has a built-in task runner)
[tasks.dev]
run = "bun run dev"
description = "Start development server"

[tasks.test]
run = "bun test"
description = "Run tests"

[tasks.lint]
run = "bun run biome check ."
description = "Run linter"

[tasks.db-reset]
run = """
dropdb myapp_dev --if-exists
createdb myapp_dev
bun run migrate
bun run seed
"""
description = "Reset development database"
# Run tasks
mise run dev
mise run test
mise run db-reset

just: A Modern Command Runner

just is a command runner (not a build system). It replaces Makefiles for running project commands without the tab-sensitivity, implicit variables, and build-system baggage of Make. Recipes are simple, readable, and explicitly parameterized.

Installation

brew install just       # macOS
cargo install just      # via Rust
mise use just@latest    # via mise

Justfile

# justfile

# List available recipes
default:
    @just --list

# Start development server
dev:
    bun run dev

# Run tests with optional filter
test filter='':
    bun test {{filter}}

# Run linter and formatter
lint:
    bun run biome check --write .

# Build for production
build: lint test
    bun run build

# Database operations
db-up:
    docker compose up -d postgres redis

db-down:
    docker compose down

db-reset: db-up
    sleep 2
    bun run db:migrate
    bun run db:seed
    @echo "Database reset complete"

# Deploy to staging
deploy-staging: build
    rsync -avz --delete dist/ staging:/var/www/app/
    ssh staging 'systemctl restart app'

# Generate a new migration
migration name:
    bun run db:migration:create {{name}}

# Open a database shell
db-shell:
    pgcli $DATABASE_URL

# Run a one-off script
run-script name:
    bun run scripts/{{name}}.ts

# Docker operations
docker-build tag='latest':
    docker build -t myapp:{{tag}} .

docker-push tag='latest': (docker-build tag)
    docker push registry.example.com/myapp:{{tag}}

# Environment-specific configuration
set dotenv-load  # Automatically load .env file

# OS-specific recipes
[linux]
install-deps:
    sudo apt install -y postgresql-client redis-tools

[macos]
install-deps:
    brew install postgresql redis
# Usage
just              # Lists all recipes
just dev          # Start dev server
just test         # Run all tests
just test "auth"  # Run tests matching "auth"
just build        # Lint, test, then build
just deploy-staging  # Full deploy pipeline
just docker-build v1.2.3  # Build with specific tag

Why just Over Make

Feature just Make
Tab sensitivity No (uses 4 spaces or tabs) Yes (must use tabs)
Recipe parameters Native (recipe arg:) Awkward (make recipe ARG=value)
Default values Native (recipe arg='default':) Requires shell tricks
Recipe listing Built-in (just --list) Requires custom target
Dotenv loading Built-in Requires shell include
Error messages Clear Cryptic
Dependencies Explicit (build: lint test) Implicit (file-based)

Nushell: A Data-Aware Shell

Nushell reimagines the shell. Instead of passing text between commands (and parsing it with awk/sed/grep), Nushell passes structured data -- tables, records, lists. This eliminates an entire class of fragile text-parsing scripts.

Installation

brew install nushell    # macOS
cargo install nu        # via Rust
sudo dnf install nushell  # Fedora

Structured Data Pipelines

# List files sorted by size (output is a table, not text)
ls | sort-by size | reverse

# Filter and format process list
ps | where cpu > 5 | select name cpu mem | sort-by cpu | reverse

# Parse JSON naturally
open package.json | get dependencies | transpose name version

# HTTP requests return structured data
http get https://api.github.com/repos/nushell/nushell/releases
| get 0
| select tag_name published_at
| get tag_name

# CSV processing without awk
open sales.csv
| where region == "US"
| group-by product
| transpose product sales
| each { |row| { product: $row.product, total: ($row.sales | get amount | math sum) } }

Custom Commands

# ~/.config/nushell/config.nu

# Git log as a table
def gl [n: int = 10] {
    git log --oneline -n $n --format="%h|%s|%an|%ar"
    | lines
    | split column "|" hash message author date
}

# Docker containers as a table
def dps [] {
    docker ps --format "{{.Names}}|{{.Status}}|{{.Ports}}"
    | lines
    | split column "|" name status ports
}

# Quick HTTP health check
def health-check [url: string] {
    let start = date now
    let resp = http get -f $url
    let duration = (date now) - $start
    {
        url: $url
        status: $resp.status
        duration_ms: ($duration | into int | $in / 1_000_000)
    }
}

# Project switcher
def proj [name: string] {
    cd $"~/projects/($name)"
}

Nushell vs Traditional Shell

# Traditional bash: fragile text parsing
# docker ps | grep myapp | awk '{print $1}' | xargs docker stop

# Nushell: structured and readable
docker ps --format json | from json | where Names =~ "myapp" | get ID | each { docker stop $in }

Putting It All Together

Here is how these tools work together in a typical development session:

  1. Raycast/Ulauncher -- press a hotkey, type your project name, terminal opens in the project directory
  2. direnv -- environment variables load automatically (DATABASE_URL, API keys, AWS profile)
  3. mise -- correct Node, Python, and tool versions activate automatically
  4. just -- run just dev to start the development server, just test to run tests
  5. Nushell (optional) -- structured data pipelines for ad-hoc exploration and scripting

The .mise.toml and justfile get committed to git. New team members clone the repo, run mise install && just install-deps, and they are ready to go. No "follow the 47-step setup guide in Confluence" onboarding.

Recommended Starter Configuration

# Install everything (macOS)
brew install direnv mise just nushell

# Add to ~/.zshrc
eval "$(direnv hook zsh)"
eval "$(mise activate zsh)"

# In your project
cat > .mise.toml << 'EOF'
[tools]
node = "20"

[env]
NODE_ENV = "development"
EOF

cat > .envrc << 'EOF'
dotenv_if_exists
EOF

cat > justfile << 'EOF'
default:
    @just --list

dev:
    bun run dev

test:
    bun test

lint:
    bun run biome check --write .

build: lint test
    bun run build
EOF

mise install
direnv allow

These tools are investments that pay dividends every day. The initial setup takes an afternoon. The time savings are permanent. Start with direnv and just -- they have the highest ratio of benefit to learning curve. Add mise when you work across multiple language versions. Try nushell when you are ready to rethink how your shell works.