-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·683 lines (612 loc) · 27.3 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·683 lines (612 loc) · 27.3 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
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
#!/bin/bash
# ============================================================================
# install.sh - Hyperi Developer Environment Bootstrap Script
# ============================================================================
# This script bootstraps the Hyperi developer environment by:
# 1. Detecting the operating system
# 2. Installing Ansible using the native package manager
# 3. Running the Ansible playbook to configure the system
#
# USAGE:
# ./install.sh [OPTIONS]
#
# OPTIONS:
# --check Run in check mode (dry-run, no changes)
# --tags TAGS Include specific tags (alias for --tags-include)
# --tags-include TAGS Include specific tags to run (comma-separated)
# --tags-exclude TAGS Exclude specific tags from running (comma-separated)
# --region REGION Apply regional settings (e.g. au, en_AU.UTF-8)
# --branch BRANCH Git branch to use (default: main)
# Personas: --soe / --contributor / --full-stack / --infra / --languages [list]
# --help Show this help message
#
# SUPPORTED PLATFORMS:
# - Ubuntu 24.04 LTS and later
# - Fedora 42 and later
# - macOS (Homebrew)
#
# LICENSE:
# Licensed under the Apache License, Version 2.0
# See LICENSE file for full license text
# ============================================================================
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Output functions
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[OK]${NC} $1"; }
print_info() { echo -e "[INFO] $1"; }
print_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
show_help() {
cat << 'EOF'
Usage: ./install.sh [OPTIONS]
OPTIONS:
--check Run in check mode (dry-run, no changes)
--tags TAGS Include specific tags (alias for --tags-include)
--tags-include TAGS Include specific tags to run (comma-separated)
--tags-exclude TAGS Exclude specific tags from running (comma-separated)
--branch BRANCH Git branch to use (default: main)
--region REGION Apply regional settings (e.g. au, en_AU.UTF-8)
--soe Shortcut: HyperI staff workstation defaults
(generic dev + CI toolchain + HyperI org policy;
no IaC, no language toolchains)
Equivalent to:
--tags developer-gui,soe,soe-gui,winlike
--contributor Shortcut: for outside contributors to HyperI products.
The dev base + the toolchain our CI runs, and none of
our org policy (no VPN, telemetry, branding, Slack).
Equivalent to: --tags contributor
--full-stack App dev, front-to-back: clean base + GUI editors + node +
typescript + python + infrastructure (kubectl/k9s).
--infra SRE / platform box: clean base + infrastructure
(cloud, IaC, k8s, the data group).
--languages [LIST] Language toolchains. Bare installs them all; or pass a
comma list, e.g. --languages rust,go
(rust/go/python/node/typescript/c).
--list-apps Print every per-app / per-group sub-tag (slack, vscode,
data, vpn-clients, ...) for granular --tags X selection
--help Show this help message
NOTE:
Run --list-apps for the full per-role and per-app tag list.
There is no longer a kitchen-sink shortcut. Pick what you want via
--tags. Default (no flags) is the lightweight CLI dev base.
EXAMPLES:
Default installation (lightweight CLI dev base):
./install.sh
Generic dev base + GUI editors + Python:
./install.sh --tags developer,developer-gui,developer-python
Just Slack and nothing else:
./install.sh --tags slack
HyperI staff machine + Rust:
./install.sh --soe --tags developer-rust
HyperI SRE workstation:
./install.sh --soe --tags infrastructure
Outside contributor working on a HyperI product:
./install.sh --contributor
Install with Australian region (locale, formats, spell-check):
./install.sh --region au
SRE / platform workstation:
./install.sh --infra
Rust + Go toolchains only:
./install.sh --languages rust,go
Install the RDP server (GNOME Remote Login) for inbound access:
./install.sh --tags rdp-server
Apply the user-level settings for named users instead of the detected ones:
./install.sh --users hyperi,ubuntu
Dry-run to see what would change:
./install.sh --check
NOTES:
- winlike (Windows-style GNOME taskbar) is the default UI mode
- If both winlike and maclike are specified, winlike wins
- RDP configures GNOME Remote Login (NOT Desktop Sharing) with a per-host
random password, shown once, and never overwrites credentials already set
- Use --tags-exclude to skip specific tags within a chosen group
- Use --list-apps to see every per-app sub-tag for granular installs
- The AI agent tools (--tags developer-ai) install but do not sign in: codex
login is interactive and per-person, so the deploy leaves it to you
- User-level settings (shell config, ~/.cargo, ~/.local, dconf, the container
stacks) are applied for every account a person works in. Skipped by default:
root, the system ranges, and the cloud image's own account (ubuntu,
cloud-user). Name any set yourself with --users, that account included.
System-wide work happens once either way.
EOF
exit 0
}
list_apps() {
cat << 'EOF'
Per-app / per-group sub-tags - pass via --tags <name> to install just that set.
Generic dev base (developer) - the additive default:
apparmor Ubuntu AppArmor userns fix
repository OS repo mirrors / fastestmirror
docker Docker (Engine on Linux, CLI on macOS - no Desktop)
utilities CLI utilities (htop, ripgrep, fd, fzf, jq, yq, ...)
git Latest Git via PPA / Fedora / brew
region Locale + hunspell (gated by --region too)
astral Astral suite: uv + ruff + ty (base component)
chrome Google Chrome (opt-in / soe)
brave Brave browser (opt-in / soe)
avatar User avatar (opt-in / soe)
removals Remove retired tools (opt-in / soe only)
update_command hyperi-update command + GUI launcher (opt-in / soe)
admin-scripts HyperI fleet admin scripts (opt-in / soe only)
Generic dev GUI (developer-gui):
desktop GNOME / ubuntu-desktop-minimal install if missing
vscode Visual Studio Code
ghostty Ghostty terminal + JetBrains Mono font
dbeaver DBeaver Community DB GUI
Languages (developer-<lang>; --languages [list] or developer-languages for all):
developer-rust rustup + cargo tools + protoc/librdkafka build deps
developer-go Go + gopls, dlv, golangci-lint, gosec, govulncheck
developer-python mypy (opt-in; ruff/ty ship in the base astral suite)
developer-node eslint + prettier (Node itself is in the base -- it is
core tooling, needed by semantic-release and CI)
developer-typescript typescript + tsx + ts-node (pulls developer-node)
developer-c C/C++ build tools
Infrastructure (infrastructure):
cloud OpenTofu, OpenBao, AWS CLI v2, checkov, terraform-docs
azure Azure CLI
gcloud Google Cloud CLI
k8s kubectl, helm, kubectx/kubens, k9s, kind, argocd,
kustomize, kubeconform, kube-linter, dive
data data group: clickhouse-client, rpk, valkey-cli, vector
cloudflare cloudflare group: flarectl, wrangler (flarectl builds
from source; Linux needs developer-go)
Contributor (contributor) - to work ON a HyperI product, no org policy:
hyperi-ci hyperi-ci + semgrep, alint
gitleaks Secret scanner
trivy Vuln / IaC / secret scanner
hadolint Dockerfile linter
pip-audit Python dependency audit
yamllint YAML linter
ansible-lint Ansible linter
pre-commit pre-commit runner
actionlint GitHub Actions linter
vulture Dead-code finder
typos Source spell-checker
maid Mermaid diagram validator
osv-scanner OSV vulnerability scanner
act Run GitHub Actions locally
HyperI SOE (soe, soe-gui) - org policy, includes everything above:
auto-updates unattended-upgrades / dnf-automatic
update-timer Weekly hyperi-update systemd timer
bash-history bash history auto-commit
claude Claude Code CLI - the binary only, no org policy, so
anyone can install it (developer-ai's plugin needs it)
claude-policy HyperI managed settings for Claude Code (soe only)
forgejo/codeberg tea (Forgejo/Gitea CLI)
colima macOS container daemon + Apple container (macOS only)
arcane Container management UI, localhost-only (OPT-IN:
-e soe_arcane_enabled=true; add
-e soe_arcane_long_session=true for a year-long login)
local-services Persistent local ClickHouse + Redpanda for spikes,
deployed stopped (OPT-IN:
-e soe_local_services_enabled=true)
disk-attach Sudo tool to mount a newly attached disk
telemetry-disable Disable Ubuntu Pro/ESM ads + telemetry
slack Slack desktop
office LibreOffice org office suite
nemo Nemo file manager (replaces Nautilus)
desktop-cleanup Hide duplicate apps, dedupe Flatpak/apt
gnome-extensions GNOME extensions (winlike / maclike)
AI coding agents (developer-ai) - opt-in; ai is the group tag for both:
codex OpenAI Codex CLI - the second opinion, not the driver
codex-plugin Codex plugin FOR Claude Code; needs claude + codex + node
Groups / client bundles (own tag; soe pulls them by default):
vpn-clients OpenVPN 3 + WireGuard + Tunnelblick (macOS)
openvpn just OpenVPN 3
wireguard just WireGuard
tunnelblick just Tunnelblick (macOS)
rdp-client Remmina (Linux) / Thincast (macOS)
Targeted deployment:
rdp-server GNOME Remote Login (RDP server on port 3389)
vm VM guest optimisations (QEMU agent etc.)
power-profile Sleep/idle/lid policy. Profiles: always-on (default),
vm. Select with -e power_profile=<name>
macOS-only:
bash-modern Modern Bash via Homebrew (does NOT chsh)
Composability examples:
./install.sh --tags slack Just Slack
./install.sh --tags data The data-tools group
./install.sh --tags vscode,ghostty VS Code + Ghostty only
./install.sh --soe --languages rust,go SOE + Rust + Go
./install.sh --infra SRE box
./install.sh --tags power-profile Never sleep on mains power
./install.sh --tags power-profile -e power_profile=vm
Never sleep at all (RDP guest)
EOF
exit 0
}
# Append comma-separated tags to ANSIBLE_TAGS, keeping the single --tags prefix.
# Used by --tags, --soe, --contributor and --region so the join logic lives once.
append_tags() {
if [[ -n "$ANSIBLE_TAGS" ]]; then
ANSIBLE_TAGS="--tags ${ANSIBLE_TAGS#--tags },$1"
else
ANSIBLE_TAGS="--tags $1"
fi
}
# Append one `key=value` Ansible extra var, so several of them compose instead
# of clobbering each other.
append_extra_var() {
if [[ -n "$ANSIBLE_EXTRA_VARS" ]]; then
ANSIBLE_EXTRA_VARS="$ANSIBLE_EXTRA_VARS -e $1"
else
ANSIBLE_EXTRA_VARS="-e $1"
fi
}
# cloud-init DECLARES the account it created, and the name differs per image
# family (ubuntu, cloud-user, ec2-user), so read it rather than guessing. An
# override dropped in cloud.cfg.d is not consulted -- name the users with
# --users on a machine that does that.
cloud_image_user() {
[[ -r /etc/cloud/cloud.cfg ]] || return 0
awk '/^[[:space:]]*default_user:/ { in_block = 1; next }
in_block && /^[[:space:]]*name:/ {
sub(/^[[:space:]]*name:[[:space:]]*/, ""); print; exit
}' /etc/cloud/cloud.cfg
}
# Every account a person actually works in: root, the system ranges and the
# image's own provisioning account are all out. uid 60000 is the ceiling because
# systemd allocates its own users above it.
discover_target_users() {
local cloud_user
cloud_user="$(cloud_image_user)"
getent passwd | awk -F: -v cloud="$cloud_user" '
$3 >= 1000 && $3 < 60000 &&
$1 != "root" && $1 != cloud &&
$7 !~ /(nologin|\/false|\/sync)$/ { print $1 }' | sort | tr '\n' ' '
}
# Parse arguments
ANSIBLE_CHECK=""
ANSIBLE_TAGS=""
ANSIBLE_SKIP_TAGS=""
ANSIBLE_EXTRA_VARS=""
GIT_BRANCH="main"
TARGET_USERS=""
while [[ $# -gt 0 ]]; do
case $1 in
--check)
ANSIBLE_CHECK="--check"
shift
;;
--tags|--tags-include)
append_tags "$2"
shift 2
;;
--tags-exclude)
if [[ -n "$ANSIBLE_SKIP_TAGS" ]]; then
ANSIBLE_SKIP_TAGS="$ANSIBLE_SKIP_TAGS,$2"
else
ANSIBLE_SKIP_TAGS="$2"
fi
shift 2
;;
--branch)
GIT_BRANCH="$2"
shift 2
;;
--region)
REGION_ARG="$2"
shift 2
;;
--soe)
# HyperI staff workstation default: generic dev + the CI toolchain
# + HyperI org policy. Excludes infrastructure (SRE-leaning, opt-in
# via --tags) and specific languages (too personal -- add --tags
# developer-rust etc.).
#
# soe pulls contributor pulls developer via meta dependencies, so
# naming soe here is enough; the others come with it.
#
# winlike gives the default GNOME taskbar -- soe-gui's UI-mode task
# only fires when winlike or maclike is in the run tags, so without
# it a --soe box gets the GUI apps but a bare shell. Spell the tags
# out manually (drop --soe) if you want maclike instead.
SOE_TAGS="developer-gui,soe,soe-gui,winlike"
append_tags "$SOE_TAGS"
shift
;;
--contributor)
# For someone outside HyperI working ON a HyperI product: the dev
# base plus the toolchain our CI runs, and none of our org policy.
CONTRIBUTOR_TAGS="contributor"
append_tags "$CONTRIBUTOR_TAGS"
shift
;;
--full-stack)
# App dev, front-to-back: clean base + GUI editors + node/typescript/
# python + infrastructure CLIs. Resolves via the full-stack meta-role.
append_tags "full-stack"
shift
;;
--infra)
# SRE / platform box: clean base + infrastructure (cloud, IaC, k8s,
# data). Resolves via the infra meta-role.
append_tags "infra"
shift
;;
--languages)
# Optional comma list: `--languages rust,go` installs just those
# toolchains; bare `--languages` installs them all (the
# developer-languages meta-role). Bash 3.2 safe (macOS bootstrap).
if [[ -n "${2:-}" && "$2" != -* ]]; then
LANG_TAGS=""
IFS=',' read -ra _langs <<< "$2"
for _l in "${_langs[@]}"; do
_l="$(printf '%s' "$_l" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
[[ -z "$_l" ]] && continue
case "$_l" in
rs|rust) _role="developer-rust" ;;
go|golang) _role="developer-go" ;;
py|python) _role="developer-python" ;;
js|node|nodejs) _role="developer-node" ;;
ts|typescript) _role="developer-typescript" ;;
c|cpp|c++|c-tools) _role="developer-c" ;;
*) _role="developer-$_l" ;;
esac
if [[ -n "$LANG_TAGS" ]]; then
LANG_TAGS="$LANG_TAGS,$_role"
else
LANG_TAGS="$_role"
fi
done
append_tags "$LANG_TAGS"
shift 2
else
append_tags "developer-languages"
shift
fi
;;
--users)
TARGET_USERS="$(printf '%s' "$2" | tr ',' ' ')"
shift 2
;;
--list-apps)
list_apps
;;
--help|-h)
show_help
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Handle winlike/maclike priority: winlike wins (it's the default UI mode).
# If both are specified, drop maclike from tags so the taskbar stays winlike.
if [[ "$ANSIBLE_TAGS" == *"maclike"* ]] && [[ "$ANSIBLE_TAGS" == *"winlike"* ]]; then
print_info "Both winlike and maclike specified - using winlike (winlike is default and wins)"
ANSIBLE_TAGS="${ANSIBLE_TAGS//,maclike/}"
ANSIBLE_TAGS="${ANSIBLE_TAGS//maclike,/}"
ANSIBLE_TAGS="${ANSIBLE_TAGS//maclike/}"
fi
# Handle --region: resolve short codes to full locale, add tag and extra var
if [[ -n "${REGION_ARG:-}" ]]; then
# Map short codes to full locale strings. Lowercase via tr, not ${x,,} --
# that is a Bash 4+ expansion and macOS ships Bash 3.2, and this bootstrap
# runs before any newer bash is installed.
REGION_LC=$(printf '%s' "$REGION_ARG" | tr '[:upper:]' '[:lower:]')
case "$REGION_LC" in
au|en_au|en_au.utf-8|en_au.utf8) DESKTOP_REGION="en_AU.UTF-8" ;;
us|en_us|en_us.utf-8|en_us.utf8) DESKTOP_REGION="en_US.UTF-8" ;;
gb|en_gb|en_gb.utf-8|en_gb.utf8) DESKTOP_REGION="en_GB.UTF-8" ;;
nz|en_nz|en_nz.utf-8|en_nz.utf8) DESKTOP_REGION="en_NZ.UTF-8" ;;
*)
# Accept any value as-is (assume full locale string)
DESKTOP_REGION="${REGION_ARG}"
;;
esac
# Add region tag
append_tags "region"
# Pass desktop_region as an extra var
append_extra_var "desktop_region=${DESKTOP_REGION}"
print_info "Region: ${DESKTOP_REGION}"
fi
# Detect operating system
print_info "Detecting operating system..."
if [[ -f /etc/os-release ]]; then
# shellcheck source=/dev/null
. /etc/os-release
OS_FAMILY=""
case "$ID" in
fedora)
OS_FAMILY="fedora"
print_info "Detected: Fedora $VERSION_ID"
;;
ubuntu)
OS_FAMILY="ubuntu"
print_info "Detected: Ubuntu $VERSION_ID"
;;
*)
print_error "Unsupported Linux distribution: $ID"
print_info "Supported: Ubuntu 24.04+, Fedora 42+, macOS"
exit 1
;;
esac
elif [[ "$(uname)" == "Darwin" ]]; then
OS_FAMILY="macos"
print_info "Detected: macOS $(sw_vers -productVersion)"
else
print_error "Unable to detect operating system"
exit 1
fi
# Check for sudo access
print_info "Verifying sudo access..."
if ! sudo -n true 2>/dev/null; then
print_warning "Passwordless sudo not configured"
print_info "You will be prompted for your password when needed"
sudo -v || {
print_error "Sudo access required"
exit 1
}
fi
print_success "Sudo access verified"
# Install latest Ansible in temporary Python venv (isolated from OS)
# This avoids circular dependency if playbook updates system Ansible
# The venv is created fresh each run and cleaned up after completion
TEMP_ANSIBLE_DIR=$(mktemp -d -t hyperi-ansible.XXXXXX)
ANSIBLE_BIN="$TEMP_ANSIBLE_DIR/bin/ansible-playbook"
# Remove the temp venv on ANY exit (success, failure, or early error). Without
# this, set -e aborts the script the instant the playbook fails, so a manual
# cleanup line below would never run and would leak the ~100s-MB venv in $TMPDIR.
trap 'rm -rf "$TEMP_ANSIBLE_DIR"' EXIT
print_info "Creating temporary Ansible environment (isolated from OS)..."
# Ensure Python 3 and curl are installed (prerequisites)
case "$OS_FAMILY" in
fedora)
if ! command -v python3 &>/dev/null || ! command -v curl &>/dev/null; then
sudo dnf install -y python3 python3-pip curl || {
print_error "Failed to install Python 3 or curl"
exit 1
}
fi
;;
ubuntu)
if ! command -v python3 &>/dev/null || ! command -v curl &>/dev/null; then
sudo apt-get update -qq
sudo apt-get install -y python3 python3-pip python3-venv curl || {
print_error "Failed to install Python 3 or curl"
exit 1
}
elif ! python3 -m venv --help &>/dev/null; then
# Python exists but venv module missing
sudo apt-get update -qq
sudo apt-get install -y python3-venv || {
print_error "Failed to install python3-venv"
exit 1
}
fi
;;
macos)
if ! command -v python3 &>/dev/null; then
print_error "Python 3 not found. Install from https://www.python.org or use: brew install python3"
exit 1
fi
# curl pre-installed on macOS
;;
esac
# Create temporary Python venv
python3 -m venv "$TEMP_ANSIBLE_DIR" || {
print_error "Failed to create Python venv"
exit 1
}
# Install latest Ansible + ansible-lint via pip
print_info "Installing latest Ansible + ansible-lint via pip..."
"$TEMP_ANSIBLE_DIR/bin/pip" install --upgrade pip setuptools wheel >/dev/null 2>&1
"$TEMP_ANSIBLE_DIR/bin/pip" install ansible ansible-lint >/dev/null 2>&1 || {
print_error "Failed to install Ansible + ansible-lint via pip"
exit 1
}
ANSIBLE_VERSION=$("$TEMP_ANSIBLE_DIR/bin/ansible" --version | head -1 | awk '{print $2}')
ANSIBLE_LINT_VERSION=$("$TEMP_ANSIBLE_DIR/bin/ansible-lint" --version 2>/dev/null | head -1 | awk '{print $2}')
print_success "Ansible $ANSIBLE_VERSION + ansible-lint $ANSIBLE_LINT_VERSION installed (temporary venv)"
# Determine script directory and check for ansible directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" || exit 1
# Check if ansible directory exists, clone if not
if [[ ! -d "ansible" ]]; then
print_warning "ansible/ directory not found"
print_info "Cloning ansible directory from repository (branch: $GIT_BRANCH)..."
# Download GitHub tarball (no git required)
TARBALL_URL="https://github.com/hyperi-io/hyperi-developer/archive/refs/heads/${GIT_BRANCH}.tar.gz"
print_info "Downloading from $TARBALL_URL..."
curl -fsSL "$TARBALL_URL" -o /tmp/hyperi-developer.tar.gz || {
print_error "Failed to download repository tarball from branch: $GIT_BRANCH"
exit 1
}
# Extract only the ansible directory
# Note: Branch name slashes become hyphens in tarball archive directory
ARCHIVE_DIR="hyperi-developer-${GIT_BRANCH//\//-}"
print_info "Extracting ansible directory from ${ARCHIVE_DIR}..."
tar -xzf /tmp/hyperi-developer.tar.gz --strip-components=1 "${ARCHIVE_DIR}/ansible" || {
print_error "Failed to extract ansible directory from ${ARCHIVE_DIR}"
rm -f /tmp/hyperi-developer.tar.gz
exit 1
}
# Cleanup
rm -f /tmp/hyperi-developer.tar.gz
print_success "Ansible directory downloaded successfully"
fi
# Build skip-tags argument if any
ANSIBLE_SKIP_TAGS_ARG=""
if [[ -n "$ANSIBLE_SKIP_TAGS" ]]; then
ANSIBLE_SKIP_TAGS_ARG="--skip-tags $ANSIBLE_SKIP_TAGS"
fi
# Run Ansible playbook using temp venv Ansible
print_info "Running Ansible playbook (using isolated venv Ansible)..."
print_info "Command: $ANSIBLE_BIN playbooks/main.yml -i inventories/localhost/inventory.yml $ANSIBLE_CHECK $ANSIBLE_TAGS $ANSIBLE_SKIP_TAGS_ARG $ANSIBLE_EXTRA_VARS"
cd ansible || exit 1
# Settle who gets the user-level settings -- shell config, ~/.cargo, ~/.local,
# dconf, the container stacks. macOS never separates the desktop user from the
# installing user, so there is nothing to discover there.
if [[ -z "$TARGET_USERS" ]]; then
if [[ "$OS_FAMILY" == "macos" ]]; then
TARGET_USERS="$(id -un)"
else
TARGET_USERS="$(discover_target_users)"
fi
fi
# No qualifying account means no user-level settings, full stop. Falling back to
# whoever invoked this would write dotfiles into root or the image's own
# provisioning account, which is the outcome the criteria exist to prevent.
if [[ -z "${TARGET_USERS// /}" ]]; then
print_error "No account qualifies for the user-level settings"
print_info "Skipped: root, system accounts, and the cloud image's own account"
print_info "Name one explicitly if that is not what you want: --users <name>"
exit 1
fi
print_info "User-level settings will be applied for: $TARGET_USERS"
# One pass per user rather than a loop inside the roles: the system-wide tasks
# are idempotent and no-op on later passes, and nothing user-scoped can be
# silently missed the way a forgotten loop or tag would miss it.
# The EXIT trap set above removes the temp venv on any outcome. Run the
# playbook inside the `if` condition so set -e does not abort before we can
# report a friendly failure (and the trap still fires on exit).
for target_user in $TARGET_USERS; do
print_info "Applying for $target_user ..."
# shellcheck disable=SC2086
if ! "$ANSIBLE_BIN" \
playbooks/main.yml \
-i inventories/localhost/inventory.yml \
$ANSIBLE_CHECK \
$ANSIBLE_TAGS \
$ANSIBLE_SKIP_TAGS_ARG \
$ANSIBLE_EXTRA_VARS \
-e "hyperi_target_user=$target_user"; then
print_error "Ansible playbook failed for $target_user"
exit 1
fi
done
# A failed optional component records a warning and the run carries on, so
# ansible-playbook exits 0 (playbooks/main.yml, "WHAT DID NOT INSTALL"). That is
# only acceptable while the warning is unmissable, which an unconditional
# "complete!" is not.
#
# The count comes from the applied-state stamp the playbook writes last. Check
# mode writes no stamp, so it says nothing rather than guessing.
deploy_warning_count=""
if [[ -z "$ANSIBLE_CHECK" && -r /var/lib/hyperi-developer/applied.json ]]; then
deploy_warning_count="$(python3 -c 'import json,sys; print(json.load(open("/var/lib/hyperi-developer/applied.json")).get("warnings", ""))' 2>/dev/null || true)"
fi
if [[ -n "$ANSIBLE_CHECK" ]]; then
print_success "Dry run complete. NOTHING was changed on this machine."
print_info "Re-run without --check to apply."
exit 0
fi
if [[ -n "$deploy_warning_count" && "$deploy_warning_count" != "0" ]]; then
print_warning "Installation finished with $deploy_warning_count component(s) NOT installed."
print_warning "Scroll up to 'Report what did not install' for the list and the reason."
print_warning "The rest of the environment is set up and usable."
else
print_success "Hyperi Developer Environment installation complete!"
fi
print_info ""
print_info "Next steps:"
print_info "1. Log out and back in for group memberships to take effect (Docker)"
print_info "2. Verify installation: docker --version, kubectl version, python3 --version"
print_info "3. Configure your tools (Git, AWS CLI, Azure CLI)"