Skip to content

Commit 07ff100

Browse files
Install the machine's tools, not just the config
install.sh assumed a machine that already had jq, gh, node, and the Claude Code CLI. On a new Mac it had nothing to work with, so the one-line bootstrap only half worked. setup-machine.sh installs Homebrew and the tools in four tiers: core, claude, deploy, and security behind a flag. Every tool is checked before install, so a second run is a no-op rather than a reinstall. Only git and jq decide the exit code, because a missing nuclei is not a broken machine. bootstrap.sh now runs it before install.sh, which is what makes the curl one-liner work on an empty machine. Xcode command line tools still need a GUI prompt, so the script reports that and keeps going instead of failing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e79a0e6 commit 07ff100

4 files changed

Lines changed: 244 additions & 4 deletions

File tree

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,40 @@ Run `./install.sh --dry-run` first if you want to see what it touches. It backs
1717
it overwrites to `~/.config/agents/backups/install-<timestamp>/`, and it never overwrites
1818
`secrets.env`.
1919

20-
On a machine with nothing set up yet, including a Linux sandbox, one line does the clone and
21-
the install:
20+
## A machine with nothing on it
21+
22+
One line takes a new Mac from empty to working. It installs Homebrew, the tools, the Claude
23+
Code CLI, and then this config:
2224

2325
```bash
2426
curl -fsSL https://raw.githubusercontent.com/itsvedantkumar/vstack/main/bootstrap.sh | bash
2527
```
2628

29+
`bootstrap.sh` clones vstack to `~/.vstack`, runs `setup-machine.sh`, then runs `install.sh`.
30+
Pass `--skip-deps` to install config only.
31+
32+
`setup-machine.sh` installs by tier, and checks each tool before installing it, so re-running
33+
costs seconds:
34+
35+
| Tier | Tools | For |
36+
|---|---|---|
37+
| core | `git`, `jq`, `ripgrep`, `fd`, `gh`, `node`, `bun`, `uv` | agent tooling and this installer |
38+
| claude | Claude Code CLI | the agent itself |
39+
| deploy | `vercel`, `wrangler` | the autonomous deploy chain |
40+
| security | `trivy`, `gitleaks`, `nmap`, `nuclei` | the `/security` command, add `--with-security` |
41+
42+
Only `git` and `jq` decide the exit code. A missing `nuclei` is not a broken machine.
43+
44+
Two things it cannot do for you. Xcode command line tools need a GUI prompt, so it tells you
45+
to run `xcode-select --install` and continues. OWASP ZAP is a large Java app and is left to
46+
you.
47+
48+
Check a machine without changing it:
49+
50+
```bash
51+
./setup-machine.sh --check
52+
```
53+
2754
## What lands where
2855

2956
| Component | Count | Installs to |

bootstrap.sh

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,21 @@ else
2727
git clone -q --depth 1 "$REPO" "$DIR"
2828
fi
2929

30-
chmod 755 "$DIR/install.sh"
31-
exec "$DIR/install.sh" "$@"
30+
chmod 755 "$DIR/install.sh" "$DIR/setup-machine.sh" 2>/dev/null || true
31+
32+
# A brand new machine has none of the tools this depends on, so install them first. The
33+
# dependency step is idempotent, so on a machine that already has everything it just prints
34+
# what it found. Pass --skip-deps to go straight to the config install.
35+
DEPS=1
36+
ARGS=""
37+
for a in "$@"; do
38+
if [ "$a" = "--skip-deps" ]; then DEPS=0; else ARGS="$ARGS $a"; fi
39+
done
40+
41+
if [ "$DEPS" = 1 ] && [ -x "$DIR/setup-machine.sh" ]; then
42+
"$DIR/setup-machine.sh" || { echo "bootstrap: required tools are missing, stopping" >&2; exit 1; }
43+
echo
44+
fi
45+
46+
# shellcheck disable=SC2086
47+
exec "$DIR/install.sh" $ARGS

install.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2020
BK="$HOME/.config/agents/backups/install-$(date +%Y%m%d-%H%M%S)"
2121
WITH_LAUNCHD=0
2222
DRY=0
23+
WITH_DEPS=0
2324
for a in "$@"; do
2425
case "$a" in
2526
--with-launchd) WITH_LAUNCHD=1 ;;
27+
--with-deps) WITH_DEPS=1 ;;
2628
--dry-run) DRY=1 ;;
2729
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
2830
*) echo "unknown flag: $a" >&2; exit 2 ;;
@@ -34,6 +36,13 @@ run(){ if [ "$DRY" = 1 ]; then say "would: $*"; else "$@"; fi; }
3436

3537
[ -f "$SRC/claude/settings.json" ] || { echo "error: run this from the vstack repo" >&2; exit 1; }
3638

39+
# --with-deps installs the tools first. Kept opt-in here because a normal re-install should
40+
# not reach for a package manager; bootstrap.sh turns it on for fresh machines.
41+
if [ "$WITH_DEPS" = 1 ] && [ -x "$SRC/setup-machine.sh" ]; then
42+
if [ "$DRY" = 1 ]; then "$SRC/setup-machine.sh" --dry-run; else "$SRC/setup-machine.sh"; fi
43+
echo
44+
fi
45+
3746
# jq drives the two merge steps (settings, MCP). Linux cloud sandboxes often lack it, and
3847
# skills plus hooks are still worth installing there, so degrade instead of aborting.
3948
HAVE_JQ=1

setup-machine.sh

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env bash
2+
# setup-machine.sh — install the tools vstack and its agents expect, on a machine that has
3+
# nothing. Idempotent: every tool is checked before it is installed, so a second run is a
4+
# fast no-op rather than a reinstall.
5+
#
6+
# ./setup-machine.sh core + claude + deploy tiers
7+
# ./setup-machine.sh --with-security also trivy, gitleaks, nmap, nuclei
8+
# ./setup-machine.sh --check report what is present, install nothing
9+
# ./setup-machine.sh --dry-run print what would be installed
10+
#
11+
# What each tier is for:
12+
# core git, jq, ripgrep, fd, gh, node, bun, uv — the agent tooling and this installer
13+
# claude the Claude Code CLI itself
14+
# deploy vercel, wrangler — the autonomous deploy chain
15+
# security trivy, gitleaks, nmap, nuclei — the /security command
16+
#
17+
# This script installs software. It never removes any, and it never touches your dotfiles.
18+
set -uo pipefail
19+
20+
WITH_SECURITY=0; CHECK=0; DRY=0
21+
for a in "$@"; do
22+
case "$a" in
23+
--with-security) WITH_SECURITY=1 ;;
24+
--check) CHECK=1 ;;
25+
--dry-run) DRY=1 ;;
26+
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
27+
*) echo "unknown flag: $a" >&2; exit 2 ;;
28+
esac
29+
done
30+
31+
# Absolute path first: a stripped-down PATH (cron, launchd, a bare sandbox) may not carry it,
32+
# and guessing the platform wrong would pick the wrong package manager.
33+
OS=$(/usr/bin/uname -s 2>/dev/null || uname -s 2>/dev/null || echo unknown)
34+
INSTALLED=""; SKIPPED=""; FAILED=""
35+
note(){ printf '%s\n' "$*"; }
36+
mark(){ # mark <list-name> <tool>
37+
case "$1" in
38+
ok) INSTALLED="$INSTALLED $2" ;;
39+
have) SKIPPED="$SKIPPED $2" ;;
40+
fail) FAILED="$FAILED $2" ;;
41+
esac
42+
}
43+
44+
# --- package manager -------------------------------------------------------------------------
45+
PM=""
46+
setup_pm(){
47+
if [ "$OS" = "Darwin" ]; then
48+
# Xcode command line tools carry git and the compilers Homebrew needs. The installer is a
49+
# GUI prompt, so it cannot be automated. Say so and keep going.
50+
if ! xcode-select -p >/dev/null 2>&1; then
51+
note "!! Xcode command line tools are missing. Run: xcode-select --install"
52+
note " Accept the dialog, wait for it to finish, then re-run this script."
53+
fi
54+
if command -v brew >/dev/null; then PM=brew; return 0; fi
55+
[ "$CHECK" = 1 ] && { note "-- homebrew: missing"; return 1; }
56+
[ "$DRY" = 1 ] && { note "would install homebrew"; PM=brew; return 0; }
57+
note ">> installing homebrew (may prompt for your password)"
58+
NONINTERACTIVE=1 /bin/bash -c \
59+
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
60+
|| { note "!! homebrew install failed — install it manually from https://brew.sh"; return 1; }
61+
for p in /opt/homebrew/bin/brew /usr/local/bin/brew; do
62+
[ -x "$p" ] && eval "$("$p" shellenv)"
63+
done
64+
command -v brew >/dev/null && PM=brew
65+
else
66+
for c in apt-get dnf apk; do command -v "$c" >/dev/null && { PM="$c"; break; }; done
67+
[ -z "$PM" ] && note "!! no supported package manager found"
68+
fi
69+
[ -n "$PM" ]
70+
}
71+
72+
pm_install(){ # pm_install <package>
73+
case "$PM" in
74+
brew) brew install "$1" ;;
75+
apt-get) sudo apt-get install -y -qq "$1" ;;
76+
dnf) sudo dnf install -y -q "$1" ;;
77+
apk) sudo apk add --quiet "$1" ;;
78+
*) return 1 ;;
79+
esac
80+
}
81+
82+
# ensure <command> <package> [label]
83+
ensure(){
84+
cmd="$1"; pkg="$2"; label="${3:-$1}"
85+
if command -v "$cmd" >/dev/null 2>&1; then
86+
note "-- $label: present ($(command -v "$cmd"))"; mark have "$label"; return 0
87+
fi
88+
[ "$CHECK" = 1 ] && { note "-- $label: MISSING"; mark fail "$label"; return 1; }
89+
[ "$DRY" = 1 ] && { note "would install $label ($pkg)"; mark ok "$label"; return 0; }
90+
note ">> installing $label"
91+
if pm_install "$pkg" >/dev/null 2>&1 && command -v "$cmd" >/dev/null 2>&1; then
92+
mark ok "$label"
93+
else
94+
note "!! $label failed to install"; mark fail "$label"
95+
fi
96+
}
97+
98+
# ensure_npm <command> <npm-package>
99+
ensure_npm(){
100+
cmd="$1"; pkg="$2"
101+
if command -v "$cmd" >/dev/null 2>&1; then
102+
note "-- $cmd: present"; mark have "$cmd"; return 0
103+
fi
104+
[ "$CHECK" = 1 ] && { note "-- $cmd: MISSING"; mark fail "$cmd"; return 1; }
105+
[ "$DRY" = 1 ] && { note "would install $cmd (npm -g $pkg)"; mark ok "$cmd"; return 0; }
106+
command -v npm >/dev/null || { note "!! $cmd needs npm, which is missing"; mark fail "$cmd"; return 1; }
107+
note ">> installing $cmd"
108+
if npm install -g "$pkg" >/dev/null 2>&1 && command -v "$cmd" >/dev/null 2>&1; then
109+
mark ok "$cmd"
110+
else
111+
note "!! $cmd failed to install"; mark fail "$cmd"
112+
fi
113+
}
114+
115+
# --- run -----------------------------------------------------------------------------------
116+
note "== platform: $OS"
117+
setup_pm || note "!! continuing without a package manager; most installs will fail"
118+
[ -n "$PM" ] && note "== package manager: $PM"
119+
120+
note ""
121+
note "== core"
122+
ensure git git
123+
ensure jq jq
124+
ensure rg ripgrep rg
125+
ensure fd fd
126+
ensure gh gh
127+
ensure node node
128+
ensure bun oven-sh/bun/bun bun
129+
ensure uv uv
130+
131+
note ""
132+
note "== claude code"
133+
if command -v claude >/dev/null 2>&1; then
134+
note "-- claude: present ($(command -v claude))"; mark have claude
135+
elif [ "$CHECK" = 1 ]; then
136+
note "-- claude: MISSING"; mark fail claude
137+
elif [ "$DRY" = 1 ]; then
138+
note "would install claude code"; mark ok claude
139+
else
140+
note ">> installing claude code"
141+
if curl -fsSL https://claude.ai/install.sh | bash >/dev/null 2>&1; then
142+
export PATH="$HOME/.local/bin:$PATH"
143+
command -v claude >/dev/null && mark ok claude || { note "!! installed but not on PATH — add \$HOME/.local/bin"; mark fail claude; }
144+
else
145+
note "!! claude install failed — see https://claude.ai/install"; mark fail claude
146+
fi
147+
fi
148+
149+
note ""
150+
note "== deploy"
151+
ensure_npm vercel vercel
152+
ensure_npm wrangler wrangler
153+
154+
if [ "$WITH_SECURITY" = 1 ]; then
155+
note ""
156+
note "== security"
157+
ensure trivy trivy
158+
ensure gitleaks gitleaks
159+
ensure nmap nmap
160+
ensure nuclei nuclei
161+
note " OWASP ZAP is not installed here: it is a large Java app. Get it from zaproxy.org."
162+
fi
163+
164+
# --- report ----------------------------------------------------------------------------------
165+
note ""
166+
note "== summary"
167+
[ -n "$SKIPPED" ] && note "already present:$SKIPPED"
168+
[ -n "$INSTALLED" ] && note "installed:$INSTALLED"
169+
[ -n "$FAILED" ] && note "missing:$FAILED"
170+
171+
# Only the tools vstack cannot work without decide the exit code. A missing nuclei is not a
172+
# broken machine; a missing jq or git is.
173+
REQUIRED="git jq"
174+
missing=""
175+
for r in $REQUIRED; do command -v "$r" >/dev/null 2>&1 || missing="$missing $r"; done
176+
if [ -n "$missing" ] && [ "$DRY" = 0 ]; then
177+
note ""
178+
note "REQUIRED TOOLS MISSING:$missing"
179+
exit 1
180+
fi
181+
182+
note ""
183+
if [ "$CHECK" = 1 ]; then
184+
note "check complete."
185+
else
186+
note "done. Next: ./install.sh"
187+
fi
188+
exit 0

0 commit comments

Comments
 (0)