# Install Dev Tools

> Install or update the developer tools uv (Python package/proj manager), Go (the Go toolchain), and nvm (Node Version Manager) to their latest official versions, downloaded from each tool's official source, and lay them out so their caches don't fill $HOME. Use when setting up a dev machine, when someone asks to install or upgrade uv/go/nvm, when checking whether these tools are outdated, when a machine's home directory or root filesystem is filling up with toolchain caches, or when you want a per-user (no-sudo) install of them. Audits installed versions and the filesystem layout first, then changes things only after the user confirms.

- Skill: `prabhakk-mw/install-dev-tools` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add prabhakk-mw/install-dev-tools`
- Raw SKILL.md: https://api.skillmd.com/api/skills/prabhakk-mw/install-dev-tools/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: prabhakk-mw (https://skillmd.com/u/prabhakk-mw)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/prabhakk-mw/install-dev-tools

---


# Install / update dev tools (uv, Go, nvm)

## Goal
Keep three developer tools current, installed per-user (no sudo), each fetched
from its **official** source — and placed so that their unbounded caches don't
threaten the root filesystem.

| Tool | What it is | Official source |
| ---- | ---------- | --------------- |
| `uv` | Python package & project manager | `https://astral.sh/uv/install.sh` |
| `go` | Go toolchain | `https://go.dev/dl/go<ver>.<os>-<arch>.tar.gz` |
| `nvm`| Node Version Manager | `https://github.com/nvm-sh/nvm` `install.sh` (pinned tag) |

This skill manages **nvm itself, not Node** — run `nvm install --lts` afterward.

## Why layout matters (read this first)

On these machines `$HOME` is a bind mount of a subdirectory on the **same
partition as `/`**. `df ~` and `df /` print two lines that look independent while
reporting one device, so it's easy to miss. The consequence: anything unbounded
landing in `$HOME` fills `/`, and a full root filesystem wedges the machine —
systemd, journald and `/tmp` all lose the ability to write. This has actually
happened to the user.

Go's module cache and nvm's Node versions are exactly that kind of growth:

| Path | Growth | Self-cleaning? |
| ---- | ------ | -------------- |
| `$GOPATH/pkg/mod` | unbounded; 1–2G from one build of a cloud-SDK project | **No** — Go never evicts module sources |
| `$GOCACHE` (`~/.cache/go-build`) | large but bounded | Yes, trims entries unused ~5 days |
| `$NVM_DIR/versions/node/*` | ~200M per Node version | No |
| `~/.npm` | npm's own cache; ignores XDG *and* the `~/.cache` symlink | No |

So `scripts/lib.sh` resolves an install **prefix**: `$DEV_TOOLS_PREFIX` if set,
else the first writable candidate (`/local-ssd/$USER`, `/scratch/$USER`,
`/local/$USER`) that is **on a different filesystem than `/`**, else `$HOME/.local`.
Being a separate filesystem is the whole criterion — a roomier directory on the
root partition wouldn't protect `/`. That gives two modes:

| | `volume` mode | `home` mode |
| --- | --- | --- |
| go | `$PREFIX/opt/go` | `~/.local/go` |
| uv, uvx | `$PREFIX/bin` | `~/.local/bin` |
| nvm | `$PREFIX/nvm` | `~/.nvm` |
| caches | `$PREFIX/cache` | `~/.cache` |
| GOPATH | `$PREFIX/go` | `~/go` |

Every path is overridable from the environment, and an **already-exported value
wins** — so a machine configured by a previous run is detected as configured,
not reinstalled somewhere new.

## The core rule
**Audit first, then ask before changing anything.** The audit is read-only. Each
tool is installed or moved only after the user confirms *that specific tool*.

## Workflow

### 1. Audit (read-only, always safe)
```bash
bash scripts/audit.sh
```
Reports the filesystem situation (whether `$HOME` shares `/`, free space, the
resolved prefix and every install target), then installed vs. latest official
version per tool with a verdict: `missing`, `update-available`, `up-to-date`, or
`unknown` (couldn't resolve latest — offline or a proxy blocking the GitHub API).

`--tsv` gives machine-readable rows (`name<TAB>installed<TAB>latest<TAB>verdict`).

**Run this in a login shell** (`bash -lic 'bash scripts/audit.sh'`). A
non-login shell may carry a stale environment snapshot that lacks the user's PATH
and `NVM_DIR`, making installed tools look `missing`.

### 2. Report
Show the table. Lead with the filesystem finding if `$HOME` shares `/` — that's
usually news to the user and it's what justifies the layout. Then call out which
tools are `missing` or `update-available`, with current → target versions.

### 3. Ask per tool
For each tool needing work, ask before touching it. Do **not** batch-upgrade. Skip
anything `up-to-date`. If `unknown`, say the latest couldn't be resolved and skip
unless the user gives an explicit `--version`.

### 4. Install (only what the user approved)
```bash
bash scripts/install-tool.sh <uv|go|nvm>            # latest official
bash scripts/install-tool.sh go --version 1.23.4    # a specific version
bash scripts/install-tool.sh uv --dry-run           # preview, no changes
```
The script prints a plan line, prompts for itself, and honors `--yes` when the
user already confirmed in conversation.

### 5. Wire up caches and the shell rc
```bash
bash scripts/setup-env.sh                # show the plan, change nothing
bash scripts/setup-env.sh --apply        # do it, prompting per change
```
This makes `~/.cache` a symlink to the prefix, appends a marker-delimited block
to the shell rc (`~/.bashrc.mine` if present, else `~/.bashrc`), and checks for rc
lines that would override it. Backs up the rc first; idempotent — re-running
replaces its own block rather than duplicating it.

### 6. Verify in a *new login shell*
```bash
bash -lic 'go env GOROOT GOPATH GOMODCACHE GOCACHE; command -v uv; echo $NVM_DIR; nvm ls'
```
Then prove it end-to-end: build a trivial Go program and confirm the cache landed
on the prefix and nothing appeared in `$HOME`.

## Notes & gotchas

- **Never export `GOROOT`.** The go binary derives it from its own real path,
  resolving symlinks correctly — verified through a `/usr/local/bin` symlink. A
  hardcoded `GOROOT` silently breaks builds the moment the toolchain moves, which
  is exactly how a previous setup went stale.
- **Getting `go` onto PATH.** Either add `$GO_INSTALL_DIR/bin`, or
  `sudo ln -s $GO_INSTALL_DIR/bin/{go,gofmt} /usr/local/bin/`. Symlink `gofmt`
  too — editors and `go vet` shell out to it.
- **Move nvm, don't reinstall it.** `install-tool.sh nvm` detects an existing
  `~/.nvm` that belongs on the prefix and offers to `mv` it. A fresh `install.sh`
  into a new `NVM_DIR` yields an *empty* nvm: installed Node versions and the
  `default -> lts/*` alias live in the old directory and get abandoned. Moving is
  safe — Node binaries are path-independent and global packages use
  `#!/usr/bin/env node` shebangs. Check first that nothing is running on that Node.
- **Installer-appended rc lines can silently win.** The uv and nvm installers
  append their setup to the **end of `~/.bashrc`**. When the managed block lives in
  a file that `~/.bashrc` *sources* (the usual `~/.bashrc.mine` arrangement), those
  appended lines run afterwards and override it. nvm's hardcodes
  `NVM_DIR="$HOME/.nvm"`. The symptom is nasty: `node` and `npm` keep working
  because PATH was already set, while `NVM_DIR` points at a directory that may not
  exist — nothing looks wrong until the next `nvm install`. `setup-env.sh` checks
  for this; re-check after any reinstall.
- **Symlink `~/.cache`, don't just set `XDG_CACHE_HOME`.** Plenty of tools
  hardcode `~/.cache` and ignore the variable, and the symlink covers tools
  installed in future with no further configuration.
- **Cache contents are never copied.** They're regenerable by definition, so
  `setup-env.sh` moves an existing `~/.cache` aside to `~/.cache.old` and leaves it
  for the user to delete.
- **Don't run `rm -rf`.** It's blocked in this user's permission setup. Move
  things aside and hand them a `!`-prefixed command to delete.
- **Gate rc blocks on the volume existing, never on `hostname`.** A hostname test
  protects the one machine it was written for and fails silently on every other —
  which is how an earlier `[ "$(hostname)" = '<some-host>' ]` block ended up doing
  nothing on a second machine for months, leaving `GOROOT`/`GOPATH` unset there
  while looking correct in the file.
- **Official sources only.** No third-party mirrors, no distro package managers.
- **No sudo** for installs; only the optional `/usr/local/bin` symlinks need it.
- **Go upgrades replace the install dir** wholesale per Go's official guidance —
  it does not merge over an old tree. Only `$GO_INSTALL_DIR` is touched.
- **Offline / proxied.** If latest can't be resolved the audit shows `unknown`
  (the GitHub API is often blocked while `go.dev` and `astral.sh` still work); pass
  `--version` to install a known version explicitly.
- Environment overrides: `DEV_TOOLS_PREFIX`, `GO_INSTALL_DIR`, `UV_INSTALL_DIR`,
  `NVM_DIR`, `DEV_CACHE_DIR`, `DEV_GOPATH`.

