-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·483 lines (436 loc) · 18.7 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·483 lines (436 loc) · 18.7 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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
#!/usr/bin/env bash
#
# aegis installer — zero-config, no-npm-interaction install.
#
# curl -fsSL https://raw.githubusercontent.com/automagik-dev/aegis/main/install.sh | bash
# curl -fsSL https://raw.githubusercontent.com/automagik-dev/aegis/main/install.sh | bash -s -- --version v0.1.0
# curl -fsSL https://raw.githubusercontent.com/automagik-dev/aegis/main/install.sh | bash -s -- --skip-verify # air-gapped
# curl -fsSL https://raw.githubusercontent.com/automagik-dev/aegis/main/install.sh | bash -s -- --auto-install-deps # CI / Docker: assume Y
# curl -fsSL https://raw.githubusercontent.com/automagik-dev/aegis/main/install.sh | bash -s -- --no-install-deps # never install deps
#
# Flags:
# --version <tag> Install a specific release (default: latest).
# --home <dir> Override AEGIS_HOME (default: ~/.aegis).
# --prefix <dir> Override symlink prefix (default: ~/.local).
# --skip-verify Bypass cosign + SLSA verification (air-gapped use only).
# --auto-install-deps Assume "yes" on every dep-install prompt (cosign + slsa-verifier).
# --no-install-deps Refuse to install any dep; print manual instructions and exit 1 if blocking.
# --help, -h Show this help and exit.
#
# What this does:
# 1. Detects platform (macOS/Linux, x64/arm64).
# 2. Fetches the requested release from the automagik-dev/aegis GitHub Releases
# (default: latest). Downloads the tarball + cosign .sig + .cert + SLSA
# provenance.
# 3. Verifies the cosign keyless signature (requires `cosign` on PATH unless
# --skip-verify) + SLSA provenance (requires `slsa-verifier` unless
# --skip-verify).
# 4. Extracts to $AEGIS_HOME (default: ~/.aegis/).
# 5. Symlinks $PREFIX/bin/aegis (default: ~/.local/bin/aegis) → the installed
# payload. Prints a path-setup hint if $PREFIX/bin is not already on PATH.
#
# What this does NOT do:
# - Touch your ~/.npmrc, ~/.bun, ~/.cache, or any package-manager config.
# - Install anything to /usr/local/ without sudo.
# - Contact npmjs.com. At all. Ever.
set -euo pipefail
REPO="automagik-dev/aegis"
VERSION="${AEGIS_VERSION:-latest}"
AEGIS_HOME="${AEGIS_HOME:-$HOME/.aegis}"
PREFIX="${AEGIS_PREFIX:-$HOME/.local}"
SKIP_VERIFY="${AEGIS_SKIP_VERIFY:-0}"
AUTO_INSTALL_DEPS="${AEGIS_AUTO_INSTALL_DEPS:-0}"
NO_INSTALL_DEPS="${AEGIS_NO_INSTALL_DEPS:-0}"
# ----- argument parsing ------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--version) VERSION="$2"; shift 2;;
--home) AEGIS_HOME="$2"; shift 2;;
--prefix) PREFIX="$2"; shift 2;;
--skip-verify) SKIP_VERIFY=1; shift;;
--auto-install-deps) AUTO_INSTALL_DEPS=1; shift;;
--no-install-deps) NO_INSTALL_DEPS=1; shift;;
--help|-h)
sed -n '3,35p' "$0" | sed 's/^# \?//'
exit 0;;
*)
printf 'aegis install: unknown flag: %s\n' "$1" >&2
exit 1;;
esac
done
# ----- flag-conflict resolution ---------------------------------------------
# --auto-install-deps and --no-install-deps are mutually exclusive: one says
# "always Y", the other says "never install". Detect the conflict order-
# independently and exit 2 with a clear message before any side effects.
if [ "$AUTO_INSTALL_DEPS" = "1" ] && [ "$NO_INSTALL_DEPS" = "1" ]; then
printf 'aegis install: --auto-install-deps and --no-install-deps are mutually exclusive. Pick one.\n' >&2
exit 2
fi
# ----- logging helpers -------------------------------------------------------
c_info() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
c_ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
c_warn() { printf '\033[1;33m⚠\033[0m %s\n' "$*" >&2; }
c_fail() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; exit 1; }
# ----- package-manager detection --------------------------------------------
# Populated by detect_pkg_manager on first call.
PKG_MANAGER=""
PKG_INSTALL_PREFIX=""
# Probe common package managers in priority order. Sets PKG_MANAGER to the
# detected name (brew/apt-get/dnf/yum/pacman/apk/nix) and PKG_INSTALL_PREFIX
# to the command prefix that performs an install. `sudo -E` is prepended on
# Linux managers when the caller is not root. macOS/brew never uses sudo.
# Returns 0 if a manager was detected, 1 otherwise.
detect_pkg_manager() {
# Memoize: first call wins.
if [ -n "$PKG_MANAGER" ]; then
return 0
fi
# sudo prefix for Linux managers when not root. brew deliberately never uses sudo.
local sudo_prefix=""
if [ "$(id -u)" -ne 0 ]; then
if command -v sudo >/dev/null 2>&1; then
sudo_prefix="sudo -E "
fi
fi
if command -v brew >/dev/null 2>&1; then
PKG_MANAGER="brew"
PKG_INSTALL_PREFIX="brew install"
return 0
fi
if command -v apt-get >/dev/null 2>&1; then
PKG_MANAGER="apt-get"
PKG_INSTALL_PREFIX="${sudo_prefix}apt-get install -y"
return 0
fi
if command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
PKG_INSTALL_PREFIX="${sudo_prefix}dnf install -y"
return 0
fi
if command -v yum >/dev/null 2>&1; then
PKG_MANAGER="yum"
PKG_INSTALL_PREFIX="${sudo_prefix}yum install -y"
return 0
fi
if command -v pacman >/dev/null 2>&1; then
PKG_MANAGER="pacman"
PKG_INSTALL_PREFIX="${sudo_prefix}pacman -S --noconfirm"
return 0
fi
if command -v apk >/dev/null 2>&1; then
PKG_MANAGER="apk"
PKG_INSTALL_PREFIX="${sudo_prefix}apk add"
return 0
fi
if command -v nix >/dev/null 2>&1; then
PKG_MANAGER="nix"
PKG_INSTALL_PREFIX="nix profile install nixpkgs#"
return 0
fi
if command -v nix-env >/dev/null 2>&1; then
PKG_MANAGER="nix-env"
PKG_INSTALL_PREFIX="nix-env -iA nixpkgs."
return 0
fi
return 1
}
# Format the exact command line the installer would run for a given package
# on the detected manager. Echoes to stdout. Callers must have already run
# detect_pkg_manager and confirmed it returned 0.
suggest_install_cmd() {
local pkg="$1"
case "$PKG_MANAGER" in
nix) printf '%s%s\n' "$PKG_INSTALL_PREFIX" "$pkg" ;;
nix-env) printf '%s%s\n' "$PKG_INSTALL_PREFIX" "$pkg" ;;
*) printf '%s %s\n' "$PKG_INSTALL_PREFIX" "$pkg" ;;
esac
}
# Prompt the user for [Y/n] consent. Default is Y (empty response accepts).
# Reads from /dev/tty so `curl ... | bash` still receives keystrokes. Returns
# 0 on accept, 1 on decline, 2 if no tty is available (non-interactive run).
prompt_yes_default() {
local answer=""
if [ ! -r /dev/tty ]; then
return 2
fi
# Read one line from the controlling tty. The outer 2>/dev/null suppresses
# bash's own "/dev/tty: No such device" message when no tty is attached
# (e.g., detached subshells). The return code drives behaviour.
# shellcheck disable=SC2162 # we want raw read; -r handles backslashes.
if ! { IFS= read -r answer < /dev/tty; } 2>/dev/null; then
return 2
fi
case "$answer" in
""|y|Y|yes|YES|Yes) return 0 ;;
*) return 1 ;;
esac
}
# Run the resolved install command and re-probe for the binary. Shared by the
# required and optional offer helpers. Calls c_fail if the install fails or
# the binary is still missing afterwards (treated as a hard error in both
# cases — once we agreed to install, partial failure is unrecoverable).
run_install_cmd() {
local cmd="$1"
local pkg="$2"
local label="$3"
c_info "Installing ${label} via ${PKG_MANAGER}..."
# Run via bash -c so the sudo prefix + arguments parse correctly.
# stdio inherited: the user sees pkg-manager output + any sudo prompt.
if ! bash -c "$cmd"; then
c_fail "${label} install failed via ${PKG_MANAGER}. Re-run after fixing, or rerun with --skip-verify (not recommended)."
fi
if ! command -v "$pkg" >/dev/null 2>&1; then
c_fail "${label} still not on PATH after install. Check the ${PKG_MANAGER} output above; you may need to open a new shell or adjust PATH."
fi
c_ok "${label} installed."
}
# Offer to install a REQUIRED missing dependency (e.g., cosign) using the
# detected package manager. Default-Y prompt; missing dep blocks install.
# Arguments: <pkg-name> <human-friendly-label>
# Honors:
# - $AUTO_INSTALL_DEPS=1 : skip prompt, log + install.
# - $NO_INSTALL_DEPS=1 : skip prompt, print manual instructions + exit 1.
# Behaviour:
# - No pkg manager detected -> returns 1 (caller handles fallback).
# - User accepts -> runs the install command inheriting stdio, re-probes,
# returns 0 on success or calls c_fail on failure (stderr preserved).
# - User declines -> prints manual-install command + exits 1.
# - No tty available (non-interactive pipe) -> prints manual command + exits 1
# with a hint about --auto-install-deps.
offer_install_dep() {
local pkg="$1"
local label="$2"
if ! detect_pkg_manager; then
return 1
fi
local cmd
cmd="$(suggest_install_cmd "$pkg")"
# --no-install-deps: refuse before any prompt fires.
if [ "$NO_INSTALL_DEPS" = "1" ]; then
c_warn "${label} not on PATH and --no-install-deps set."
printf 'To install %s manually, run:\n' "$label" >&2
printf ' %s\n' "$cmd" >&2
c_fail "Cannot continue without ${label}. Re-run without --no-install-deps, or pass --skip-verify (not recommended)."
fi
# --auto-install-deps: skip the prompt, log loudly, and install.
if [ "$AUTO_INSTALL_DEPS" = "1" ]; then
printf 'auto-install-deps mode: will install %s without prompting\n' "$label" >&2
c_warn "${label} not on PATH."
c_info "Will run: ${cmd}"
run_install_cmd "$cmd" "$pkg" "$label"
return 0
fi
c_warn "${label} not on PATH."
printf 'Will run: %s. [Y/n] ' "$cmd"
set +e
prompt_yes_default
local decision=$?
set -e
case "$decision" in
0)
run_install_cmd "$cmd" "$pkg" "$label"
return 0
;;
1)
printf '\n' >&2
c_warn "Declined. To install ${label} manually, run:"
printf ' %s\n' "$cmd" >&2
c_fail "Cannot continue without ${label}. Re-run this installer after installing it, or pass --skip-verify (not recommended)."
;;
2)
printf '\n' >&2
c_warn "No interactive terminal available (non-interactive pipe). To install ${label} manually, run:"
printf ' %s\n' "$cmd" >&2
c_fail "Cannot prompt for consent without a tty. Install ${label} first, pass --auto-install-deps to install unattended, or pass --skip-verify (not recommended)."
;;
esac
}
# Offer to install an OPTIONAL missing dependency (e.g., slsa-verifier) using
# the detected package manager. Default-N prompt; declining is fine — caller
# proceeds without the dep.
# Arguments: <pkg-name> <human-friendly-label> <prompt-question>
# Honors:
# - $AUTO_INSTALL_DEPS=1 : skip prompt, log + install.
# - $NO_INSTALL_DEPS=1 : skip prompt, log a warning, return 1 (no exit).
# Returns 0 if the dep ended up installed, 1 otherwise. Never calls c_fail
# unless an install we agreed to actually fails — optional deps must not
# block the primary install path.
offer_install_optional_dep() {
local pkg="$1"
local label="$2"
local question="$3"
if ! detect_pkg_manager; then
return 1
fi
local cmd
cmd="$(suggest_install_cmd "$pkg")"
# --no-install-deps: silently skip with a brief note; optional deps never
# cause exit 1 on this flag (only required deps do).
if [ "$NO_INSTALL_DEPS" = "1" ]; then
c_warn "${label} not on PATH and --no-install-deps set; skipping optional install. Manual command: ${cmd}"
return 1
fi
# --auto-install-deps: opt the user in.
if [ "$AUTO_INSTALL_DEPS" = "1" ]; then
printf 'auto-install-deps mode: will install %s without prompting\n' "$label" >&2
c_info "Will run: ${cmd}"
run_install_cmd "$cmd" "$pkg" "$label"
return 0
fi
# Interactive default-N prompt. Empty answer == decline.
printf '%s [y/N] ' "$question"
local answer=""
if [ ! -r /dev/tty ]; then
printf '\n' >&2
c_warn "No tty for optional ${label} prompt; skipping. Pass --auto-install-deps to install unattended."
return 1
fi
# shellcheck disable=SC2162 # we want raw read; -r handles backslashes.
if ! { IFS= read -r answer < /dev/tty; } 2>/dev/null; then
printf '\n' >&2
c_warn "Could not read optional ${label} response; skipping."
return 1
fi
case "$answer" in
y|Y|yes|YES|Yes)
c_info "Will run: ${cmd}"
run_install_cmd "$cmd" "$pkg" "$label"
return 0
;;
*)
c_warn "Skipping optional ${label} install. Manual command: ${cmd}"
return 1
;;
esac
}
# ----- platform detection ----------------------------------------------------
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$OS" in
linux|darwin) ;;
*) c_fail "Unsupported OS: $OS (aegis supports macOS and Linux; WSL runs as linux).";;
esac
case "$ARCH" in
x86_64|amd64) ARCH=x64;;
arm64|aarch64) ARCH=arm64;;
*) c_fail "Unsupported arch: $ARCH (aegis supports x64 and arm64).";;
esac
c_info "Platform detected: $OS/$ARCH"
# ----- dependency checks -----------------------------------------------------
for cmd in curl tar node; do
command -v "$cmd" >/dev/null 2>&1 || c_fail "Missing required tool: $cmd"
done
NODE_MAJOR=$(node --version | sed 's/^v//' | cut -d. -f1)
[ "$NODE_MAJOR" -ge 20 ] || c_fail "aegis requires Node.js >=20 (you have $(node --version))"
if [ "$SKIP_VERIFY" != "1" ]; then
# Single upfront probe for cosign. If missing, try to auto-install via the
# host's package manager with explicit user consent. When no supported pkg
# manager is detected, fall back to the original manual-install error.
if ! command -v cosign >/dev/null 2>&1; then
if ! offer_install_dep "cosign" "cosign"; then
if [ "$NO_INSTALL_DEPS" = "1" ]; then
c_warn "cosign not on PATH and --no-install-deps set."
printf 'Install cosign manually from https://github.com/sigstore/cosign/releases\n' >&2
c_fail "Cannot continue without cosign. Re-run without --no-install-deps, or pass --skip-verify (not recommended)."
fi
c_fail "cosign not on PATH and no supported package manager detected (brew/apt-get/dnf/yum/pacman/apk/nix). Install from https://github.com/sigstore/cosign/releases or rerun with --skip-verify (not recommended)."
fi
fi
# slsa-verifier is OPTIONAL. If missing AND we have a pkg manager, offer
# auto-install with default-N (must be opt-in). If no pkg manager, keep the
# original warning-only behaviour and continue without it.
if ! command -v slsa-verifier >/dev/null 2>&1; then
if ! offer_install_optional_dep "slsa-verifier" "slsa-verifier" "Install slsa-verifier for full 3-layer attestation check?"; then
c_warn "slsa-verifier not found — SLSA provenance check will be skipped. Install from https://github.com/slsa-framework/slsa-verifier/releases for the full 3-layer attestation check."
fi
fi
fi
# ----- resolve release -------------------------------------------------------
if [ "$VERSION" = "latest" ]; then
VERSION=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | \
grep '"tag_name":' | head -1 | sed 's/.*"tag_name": "\(.*\)".*/\1/')
[ -n "$VERSION" ] || c_fail "Could not resolve latest release tag from GitHub API."
fi
case "$VERSION" in
v*) ;;
*) VERSION="v$VERSION";;
esac
c_info "Installing aegis $VERSION"
# ----- download tarball + signatures -----------------------------------------
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
BASE_URL="https://github.com/$REPO/releases/download/$VERSION"
# npm pack names the tarball `automagik-dev-aegis-<version-without-v>.tgz` for the
# @automagik-dev scope. We try a few patterns so a future rename doesn't break us.
VERSION_BARE="${VERSION#v}"
CANDIDATES=(
"automagik-dev-aegis-${VERSION_BARE}.tgz"
"automagik-dev-aegis-${VERSION}.tgz"
"aegis-${VERSION_BARE}.tgz"
)
TARBALL=""
for candidate in "${CANDIDATES[@]}"; do
c_info "Probing release asset: $candidate"
if curl -fsSLI "$BASE_URL/$candidate" >/dev/null 2>&1; then
TARBALL="$candidate"
break
fi
done
[ -n "$TARBALL" ] || c_fail "Could not find an aegis tarball in release $VERSION. See https://github.com/$REPO/releases/$VERSION for the actual asset name."
c_info "Downloading $TARBALL..."
curl -fsSL "$BASE_URL/$TARBALL" -o "$TMP/$TARBALL"
curl -fsSL "$BASE_URL/$TARBALL.sig" -o "$TMP/$TARBALL.sig" 2>/dev/null || c_warn "No .sig asset — cosign verification cannot run."
curl -fsSL "$BASE_URL/$TARBALL.cert" -o "$TMP/$TARBALL.cert" 2>/dev/null || c_warn "No .cert asset — cosign verification cannot run."
curl -fsSL "$BASE_URL/provenance.intoto.jsonl" -o "$TMP/provenance.intoto.jsonl" 2>/dev/null || c_warn "No provenance asset — SLSA verification cannot run."
# ----- verify ----------------------------------------------------------------
if [ "$SKIP_VERIFY" = "1" ]; then
c_warn "--skip-verify passed: cosign and SLSA verification BYPASSED. Only safe for air-gapped mirrors with out-of-band verification."
else
c_info "Verifying cosign keyless signature..."
cosign verify-blob \
--certificate-identity-regexp "^https://github.com/$REPO/\\.github/workflows/release\\.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--signature "$TMP/$TARBALL.sig" \
--certificate "$TMP/$TARBALL.cert" \
"$TMP/$TARBALL" >/dev/null 2>&1 || c_fail "cosign signature verification FAILED. Do not install."
c_ok "cosign signature verified (OIDC identity pinned to release.yml@$VERSION)"
if command -v slsa-verifier >/dev/null 2>&1 && [ -f "$TMP/provenance.intoto.jsonl" ]; then
c_info "Verifying SLSA L3 provenance..."
slsa-verifier verify-artifact "$TMP/$TARBALL" \
--provenance-path "$TMP/provenance.intoto.jsonl" \
--source-uri "github.com/$REPO" >/dev/null 2>&1 || c_fail "SLSA provenance verification FAILED. Do not install."
c_ok "SLSA L3 provenance verified"
fi
fi
# ----- install --------------------------------------------------------------
INSTALL_DIR="$AEGIS_HOME/$VERSION"
c_info "Extracting to $INSTALL_DIR..."
mkdir -p "$INSTALL_DIR"
tar -xzf "$TMP/$TARBALL" -C "$INSTALL_DIR" --strip-components=1
c_info "Installing dependencies..."
( cd "$INSTALL_DIR" && npm install --omit=dev --no-audit --no-fund --silent >/dev/null 2>&1 ) || c_fail "npm install of aegis dependencies failed inside $INSTALL_DIR."
# Record install metadata for `aegis update` to use later.
cat > "$AEGIS_HOME/install.json" <<EOF
{
"version": "$VERSION",
"installed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"install_dir": "$INSTALL_DIR",
"platform": "$OS/$ARCH",
"skip_verify": $SKIP_VERIFY
}
EOF
chmod 600 "$AEGIS_HOME/install.json"
# Atomic symlink swap.
mkdir -p "$PREFIX/bin"
ln -sfn "$INSTALL_DIR/dist/cli.js" "$PREFIX/bin/aegis.tmp"
chmod +x "$INSTALL_DIR/dist/cli.js"
mv "$PREFIX/bin/aegis.tmp" "$PREFIX/bin/aegis"
c_ok "aegis $VERSION installed to $INSTALL_DIR"
c_ok "Symlinked: $PREFIX/bin/aegis"
case ":$PATH:" in
*":$PREFIX/bin:"*) ;;
*)
c_warn "$PREFIX/bin is not on your PATH. Add it to your shell profile:"
printf ' export PATH="%s/bin:$PATH"\n' "$PREFIX" >&2;;
esac
printf '\n%s\n' "Next: try 'aegis --help' or 'aegis scan --all-homes --root \"\$PWD\"'"