A good project script can run twice without making the second run strange.
Local work is interrupted. Ports remain open. A file already exists. A migration partially runs. The next invocation should converge on the desired state instead of making the mess larger.
Check before creating:
if [ ! -d "node_modules" ]; then
pnpm install
fi
if ! docker volume inspect app_pgdata >/dev/null 2>&1; then
docker volume create app_pgdata
fi
Stable names make cleanup possible: container, volume, network, log, lock, and generated-file paths should be predictable.
Assume failure in the middle. Print phases so the user can locate the stop:
checking dependencies
starting database
waiting for health check
running migrations
seeding fixtures
writing env file
done
Idempotent does not mean harmless. Keep destructive work separate: init, serve, stop, reset, and test should have distinct commands. A reset should say clearly that it removes local state.
Locks prevent duplicate work:
LOCKFILE=".tmp/init.lock"
if [ -e "$LOCKFILE" ]; then
echo "init is already running or exited uncleanly: $LOCKFILE"
exit 1
fi
mkdir -p .tmp
trap 'rm -f "$LOCKFILE"' EXIT
touch "$LOCKFILE"
Check prerequisites before mutating. A missing Docker daemon or wrong Node version should fail before half the environment is written.
The output should be a receipt:
database: already running
migrations: up to date
fixtures: seeded 14 records
server: http://localhost:4321
Run the script, run it again, interrupt it, and run it again. Recovery is the real test. Good scripts make that recovery boring.
Related posts

About Jeremy London
Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.