-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsetup-machine.sh
More file actions
executable file
·406 lines (382 loc) · 17 KB
/
Copy pathsetup-machine.sh
File metadata and controls
executable file
·406 lines (382 loc) · 17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#!/usr/bin/env bash
# setup-machine.sh — install the tools vstack and its agents expect, on a machine that has
# nothing. Idempotent: every tool is checked before it is installed, so a second run is a
# fast no-op rather than a reinstall.
#
# ./setup-machine.sh core + claude
# ./setup-machine.sh --with-deploy also vercel and wrangler
# ./setup-machine.sh --with-security also trivy, gitleaks, nmap, nuclei
# ./setup-machine.sh --with-plugins also frontend-design, typescript-lsp (below)
# ./setup-machine.sh --check report what is present, install nothing
# ./setup-machine.sh --dry-run print what would be installed
#
# What each tier is for:
# core git, jq, ripgrep, fd, gh, node, bun, uv — the agent tooling and this installer
# bundled npm, npx, pnpm, yarn, python3 — verified, not installed: they come
# with node or the Xcode tools
# claude the Claude Code CLI itself
# conductor the Conductor Mac app, several agents in parallel (macOS only)
# deploy vercel, wrangler — opt-in. Someone who wants better
# Claude behaviour does not need this
# author's deployment stack, and
# installing it by default made a
# personal toolchain look like a
# requirement of the product.
# security trivy, gitleaks, nmap, nuclei — the /security command
# plugins frontend-design, typescript-lsp — opt-in. Neither is vstack's code; both come
# from anthropics/claude-plugins-official
# and update on their own schedule. A
# headline curl-pipe command installing
# software from outside this repo with no
# flag and no mention in the README was a
# consent problem, not a documentation
# one. VSTACK_PLUGINS=1 opts in the same
# way for the bootstrap.sh one-liner,
# which cannot take flags meant for this
# script.
#
# This script installs software. It never removes any, and it never touches your dotfiles or any
# file it does not ship.
set -uo pipefail
WITH_SECURITY=0; WITH_DEPLOY=0; WITH_PLUGINS=0; CHECK=0; DRY=0; APT_UPDATED=0
for a in "$@"; do
case "$a" in
--with-security) WITH_SECURITY=1 ;;
--with-deploy) WITH_DEPLOY=1 ;;
--with-plugins) WITH_PLUGINS=1 ;;
--check) CHECK=1 ;;
--dry-run) DRY=1 ;;
-h|--help) sed -n '2,35p' "$0"; exit 0 ;;
*) echo "unknown flag: $a" >&2; exit 2 ;;
esac
done
# VSTACK_PLUGINS=1 is the env-var form of --with-plugins, for callers that cannot pass this
# script a flag of its own — the bootstrap.sh one-liner forwards its arguments to install.sh,
# not to this script, so a flag here would never reach it from that entrypoint.
[ "${VSTACK_PLUGINS:-0}" = 1 ] && WITH_PLUGINS=1
# Absolute path first: a stripped-down PATH (cron, launchd, a bare sandbox) may not carry it,
# and guessing the platform wrong would pick the wrong package manager.
OS=$(/usr/bin/uname -s 2>/dev/null || uname -s 2>/dev/null || echo unknown)
INSTALLED=""; SKIPPED=""; FAILED=""
note(){ printf '%s\n' "$*"; }
mark(){ # mark <list-name> <tool>
case "$1" in
ok) INSTALLED="$INSTALLED $2" ;;
have) SKIPPED="$SKIPPED $2" ;;
fail) FAILED="$FAILED $2" ;;
esac
}
# --- package manager -------------------------------------------------------------------------
PM=""
setup_pm(){
if [ "$OS" = "Darwin" ]; then
# Xcode command line tools carry git and the compilers Homebrew needs. The installer is a
# GUI prompt, so it cannot be automated. Say so and keep going.
if ! xcode-select -p >/dev/null 2>&1; then
note "!! Xcode command line tools are missing. Run: xcode-select --install"
note " Accept the dialog, wait for it to finish, then re-run this script."
fi
if command -v brew >/dev/null; then PM=brew; return 0; fi
[ "$CHECK" = 1 ] && { note "-- homebrew: missing"; return 1; }
[ "$DRY" = 1 ] && { note "would install homebrew"; PM=brew; return 0; }
note ">> installing homebrew (may prompt for your password)"
NONINTERACTIVE=1 /bin/bash -c \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
|| { note "!! homebrew install failed — install it manually from https://brew.sh"; return 1; }
for p in /opt/homebrew/bin/brew /usr/local/bin/brew; do
[ -x "$p" ] && eval "$("$p" shellenv)"
done
command -v brew >/dev/null && PM=brew
else
for c in apt-get dnf apk; do command -v "$c" >/dev/null && { PM="$c"; break; }; done
[ -z "$PM" ] && note "!! no supported package manager found"
fi
[ -n "$PM" ]
}
pm_install(){ # pm_install <package>
case "$PM" in
brew) brew install "$1" ;;
apt-get)
if [ "$EUID" -ne 0 ] && ! command -v sudo >/dev/null 2>&1; then
return 1
fi
if [ "$EUID" -ne 0 ]; then
sudo apt-get install -y -qq "$1"
else
apt-get install -y -qq "$1"
fi
;;
dnf)
if [ "$EUID" -ne 0 ] && ! command -v sudo >/dev/null 2>&1; then
return 1
fi
if [ "$EUID" -ne 0 ]; then
sudo dnf install -y -q "$1"
else
dnf install -y -q "$1"
fi
;;
apk)
if [ "$EUID" -ne 0 ] && ! command -v sudo >/dev/null 2>&1; then
return 1
fi
if [ "$EUID" -ne 0 ]; then
sudo apk add --quiet "$1"
else
apk add --quiet "$1"
fi
;;
*) return 1 ;;
esac
}
apt_update_guard(){
[ "$PM" != "apt-get" ] && return 0
[ "$APT_UPDATED" = 1 ] && return 0
if [ "$DRY" = 1 ]; then
note "would run: apt-get update"; return 0
fi
if [ "$EUID" -ne 0 ] && ! command -v sudo >/dev/null 2>&1; then
note "!! cannot run apt-get update: not root and sudo not available"
return 1
fi
if [ "$EUID" -ne 0 ]; then
sudo apt-get update -qq >/dev/null 2>&1 || return 1
else
apt-get update -qq >/dev/null 2>&1 || return 1
fi
APT_UPDATED=1
}
# ensure <command> <package-brew> <package-apt> [label]
# on apt systems, uses package-apt; on brew, uses package-brew; others use package-brew
ensure(){
cmd="$1"; pkg_brew="$2"; pkg_apt="${3:-$2}"; label="${4:-$1}"
if command -v "$cmd" >/dev/null 2>&1; then
note "-- $label: present ($(command -v "$cmd"))"; mark have "$label"; return 0
fi
[ "$CHECK" = 1 ] && { note "-- $label: MISSING"; mark fail "$label"; return 1; }
pkg="$pkg_brew"
[ "$PM" = "apt-get" ] && pkg="$pkg_apt"
[ "$DRY" = 1 ] && { note "would install $label ($pkg)"; mark ok "$label"; return 0; }
note ">> installing $label"
if pm_install "$pkg" >/dev/null 2>&1 && command -v "$cmd" >/dev/null 2>&1; then
mark ok "$label"
else
note "!! $label failed to install"; mark fail "$label"
fi
}
# ensure_npm <command> <npm-package>
ensure_npm(){
cmd="$1"; pkg="$2"
if command -v "$cmd" >/dev/null 2>&1; then
note "-- $cmd: present"; mark have "$cmd"; return 0
fi
[ "$CHECK" = 1 ] && { note "-- $cmd: MISSING"; mark fail "$cmd"; return 1; }
[ "$DRY" = 1 ] && { note "would install $cmd (npm -g $pkg)"; mark ok "$cmd"; return 0; }
command -v npm >/dev/null || { note "!! $cmd needs npm, which is missing"; mark fail "$cmd"; return 1; }
note ">> installing $cmd"
if npm install -g "$pkg" >/dev/null 2>&1 && command -v "$cmd" >/dev/null 2>&1; then
mark ok "$cmd"
else
note "!! $cmd failed to install"; mark fail "$cmd"
fi
}
# ensure_remote <command> <installer-url> [label]
# For tools like bun and uv that provide curl installers outside package managers
ensure_remote(){
cmd="$1"; url="$2"; label="${3:-$1}"
if command -v "$cmd" >/dev/null 2>&1; then
note "-- $label: present ($(command -v "$cmd"))"; mark have "$label"; return 0
fi
[ "$CHECK" = 1 ] && { note "-- $label: MISSING"; mark fail "$label"; return 1; }
[ "$DRY" = 1 ] && { note "would install $label (via curl)"; mark ok "$label"; return 0; }
note ">> installing $label"
if curl -fsSL "$url" | bash >/dev/null 2>&1 && command -v "$cmd" >/dev/null 2>&1; then
mark ok "$label"
else
note "!! $label failed to install"; mark fail "$label"
fi
}
# --- run -----------------------------------------------------------------------------------
note "== platform: $OS"
setup_pm || note "!! continuing without a package manager; most installs will fail"
[ -n "$PM" ] && note "== package manager: $PM"
note ""
note "== core"
apt_update_guard
ensure git git git
ensure jq jq jq
ensure rg ripgrep ripgrep rg
ensure fd fd fd-find fd
ensure gh gh gh
ensure node node nodejs node
ensure_remote bun "https://bun.sh/install.sh"
ensure_remote uv "https://astral.sh/uv/install.sh"
note ""
note "== bundled with node"
# npm, npx and corepack arrive with node. Installing them separately fights the node install,
# so this section verifies rather than installs. It exists because ensure_npm below needs npm,
# and "vercel needs npm, which is missing" is a confusing way to learn that node is broken.
for c in npm npx; do
if command -v "$c" >/dev/null 2>&1; then
note "-- $c: present ($(command -v "$c"))"; mark have "$c"
else
note "!! $c: missing, which means the node install is incomplete"; mark fail "$c"
fi
done
# corepack turns on pnpm and yarn without downloading either. Plenty of repos assume one of
# them and fail their install step without it.
if ! command -v corepack >/dev/null 2>&1; then
note "-- corepack: not available, skipping pnpm and yarn"
elif [ "$CHECK" = 1 ]; then
command -v pnpm >/dev/null 2>&1 && note "-- pnpm: present" || note "-- pnpm: MISSING (corepack enable pnpm)"
elif [ "$DRY" = 1 ]; then
note "would enable pnpm and yarn through corepack"
else
if corepack enable pnpm yarn >/dev/null 2>&1; then
note ">> enabled pnpm and yarn (corepack)"; mark ok "pnpm/yarn"
else
note "!! corepack enable failed; run it yourself if a repo needs pnpm or yarn"
fi
fi
# python3 ships with the Xcode command line tools on macOS, and uv manages versions and venvs
# from there. Checked, not installed.
if command -v python3 >/dev/null 2>&1; then
note "-- python3: present ($(command -v python3))"; mark have python3
else
note "!! python3: missing, install the Xcode command line tools"; mark fail python3
fi
note ""
note "== claude code"
if command -v claude >/dev/null 2>&1; then
note "-- claude: present ($(command -v claude))"; mark have claude
elif [ "$CHECK" = 1 ]; then
note "-- claude: MISSING"; mark fail claude
elif [ "$DRY" = 1 ]; then
note "would install claude code"; mark ok claude
else
note ">> installing claude code"
if curl -fsSL https://claude.ai/install.sh | bash >/dev/null 2>&1; then
export PATH="$HOME/.local/bin:$PATH"
command -v claude >/dev/null && mark ok claude || { note "!! installed but not on PATH — add \$HOME/.local/bin"; mark fail claude; }
else
note "!! claude install failed — see https://claude.ai/install"; mark fail claude
fi
fi
note ""
note "== conductor"
# Conductor runs several Claude Code agents in parallel, each in its own git worktree. It is a
# Mac app, so a cask install is the only sane route and Linux skips it. Detection looks for the
# app bundle rather than asking brew: it is commonly installed by download, and brew would
# then report it missing and try to install it again.
if [ "$OS" != "Darwin" ]; then
note "-- conductor: skipped (macOS only)"
elif [ -d "/Applications/Conductor.app" ]; then
note "-- conductor: present (/Applications/Conductor.app)"; mark have conductor
elif [ "$CHECK" = 1 ]; then
note "-- conductor: MISSING"; mark fail conductor
elif [ "$DRY" = 1 ]; then
note "would install conductor (brew cask)"; mark ok conductor
elif [ "$PM" = brew ]; then
note ">> installing conductor"
if brew install --cask conductor >/dev/null 2>&1 && [ -d "/Applications/Conductor.app" ]; then
mark ok conductor
else
note "!! conductor failed to install; download it from https://conductor.build"; mark fail conductor
fi
else
note "!! conductor needs homebrew; download it from https://conductor.build"; mark fail conductor
fi
note ""
note "== claude plugins"
# Plugins carry the language tooling. Neither is claimed by name in settings.json's
# enabledPlugins any more -- a name there means nothing until the marketplace is added and the
# plugin actually installed, and typescript-lsp reproduced that defect the moment this script
# stopped installing it by default. install.sh's settings merge strips an enabledPlugins entry
# that is not actually present on disk (see the del(.enabledPlugins[...]) calls there), so a
# claim only survives a reinstall when the plugin really is installed. Installing them here by
# default without the same restraint would put this script back in the business install.sh
# already opted out of, from a headline curl-pipe command that never asked. --with-plugins (or
# VSTACK_PLUGINS=1) is the opt-in; the default path names what it is skipping.
#
# claude-mem was the third entry here until 1.46.0 and is gone. It was measured injecting
# nothing: its only UserPromptSubmit hook returns {} and the context builder it ships is
# reachable from no hook at all. See CHANGELOG.md 1.46.0.
if ! command -v claude >/dev/null 2>&1; then
note "-- plugins: skipped (claude not installed)"
elif [ "$WITH_PLUGINS" != 1 ]; then
present=""
for pl in frontend-design typescript-lsp; do
claude plugin list 2>/dev/null | grep -qi "$pl" && present="$present $pl"
done
[ -n "$present" ] && note "-- plugins: already present:$present"
note "-- plugins: skipped frontend-design, typescript-lsp (opt in with --with-plugins or VSTACK_PLUGINS=1)"
elif [ "$CHECK" = 1 ] || [ "$DRY" = 1 ]; then
note "${DRY:+would install }plugins: frontend-design, typescript-lsp"
else
claude plugin marketplace add anthropics/claude-plugins-official >/dev/null 2>&1 || true
for pl in frontend-design@claude-plugins-official typescript-lsp@claude-plugins-official; do
if claude plugin list 2>/dev/null | grep -q "${pl%@*}"; then
note "-- ${pl%@*}: present"; mark have "${pl%@*}"
elif claude plugin install "$pl" >/dev/null 2>&1; then
note ">> installed ${pl%@*}"; mark ok "${pl%@*}"
else
note "!! ${pl%@*} failed (add it later with: claude plugin install $pl)"; mark fail "${pl%@*}"
fi
done
fi
note ""
if [ "$WITH_DEPLOY" = 1 ]; then
note "== deploy"
ensure_npm vercel vercel
ensure_npm wrangler wrangler
else
note "== deploy (skipped — pass --with-deploy for vercel and wrangler)"
fi
if [ "$WITH_SECURITY" = 1 ]; then
note ""
note "== security"
ensure trivy trivy
ensure gitleaks gitleaks
ensure nmap nmap
ensure nuclei nuclei
note " OWASP ZAP is not installed here: it is a large Java app. Get it from zaproxy.org."
fi
# --- report ----------------------------------------------------------------------------------
note ""
note "== summary"
[ -n "$SKIPPED" ] && note "already present:$SKIPPED"
[ -n "$INSTALLED" ] && note "installed:$INSTALLED"
[ -n "$FAILED" ] && note "missing:$FAILED"
# Only the tools vstack cannot work without decide the exit code. A missing nuclei is not a
# broken machine; a missing jq or git is.
#
# `claude` belongs on that list and was not on it, which made this report success for a machine
# that cannot run the product at all. bootstrap.sh treats a zero exit as "tools are fine" and
# carries on to modify config and shell startup files, so a stranger could be told everything
# worked and then find there is no agent to run. The README promises a working setup including
# the CLI; this is the check that has to mean it.
REQUIRED="git jq claude"
missing=""
for r in $REQUIRED; do command -v "$r" >/dev/null 2>&1 || missing="$missing $r"; done
if [ -n "$missing" ] && [ "$DRY" = 0 ]; then
note ""
note "REQUIRED TOOLS MISSING:$missing"
case "$missing" in
*claude*) note " the Claude Code CLI is the product this configures; without it the install"
note " would leave you config for an agent you cannot run."
note " install: npm install -g @anthropic-ai/claude-code" ;;
esac
exit 1
fi
note ""
if [ "$CHECK" = 1 ]; then
note "check complete."
elif [ "${VSTACK_CHAINED:-0}" = 1 ]; then
# Set by bootstrap.sh, which runs this script and then execs install.sh itself in the same
# invocation. "Next: ./install.sh" was stale there -- it reads as a manual step still to do,
# for a step that is about to happen automatically one line later.
note "done. continuing to install.sh..."
else
note "done. Next: ./install.sh"
fi
exit 0