Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.3
1.0.4
8 changes: 8 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
#!/usr/bin/env bash
# install.sh — Bootstrap mbp on a fresh Mac
# Usage: bash <(curl -fsSL https://raw.githubusercontent.com/devizerio/mbp/main/install.sh)
#
# Security note: This script is fetched over HTTPS from GitHub. It trusts:
# - GitHub's TLS infrastructure for transport security
# - The devizerio/mbp repository for code integrity
# For additional verification, download first and inspect:
# curl -fsSL https://raw.githubusercontent.com/devizerio/mbp/main/install.sh -o install.sh
# shasum -a 256 install.sh # compare with published hash
# bash install.sh
set -euo pipefail

MBP_REPO="${MBP_REPO:-$HOME/.mbp/repo}"
Expand Down
24 changes: 21 additions & 3 deletions lib/audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,27 @@ audit_macos_defaults() {
# Parse apply_default lines: apply_default "domain" "key" "type" "value"
while IFS= read -r line; do
local domain key type expected_value
# Extract quoted args using parameter expansion
eval "set -- $(echo "$line" | sed 's/apply_default //')" 2>/dev/null || continue
domain="$1" key="$2" type="$3" expected_value="$4"
# Safely extract quoted args without eval
local args_str
args_str=$(echo "$line" | sed 's/^[[:space:]]*apply_default[[:space:]]*//')
# Parse space-separated quoted arguments safely using read
local -a args=()
while [[ -n "$args_str" ]]; do
args_str="${args_str#"${args_str%%[![:space:]]*}"}" # trim leading space
[ -z "$args_str" ] && break
if [[ "$args_str" == \"* ]]; then
# Quoted argument — extract up to closing quote
args_str="${args_str#\"}"
args+=("${args_str%%\"*}")
args_str="${args_str#*\"}"
else
# Unquoted argument
args+=("${args_str%% *}")
args_str="${args_str#* }"
[[ "$args_str" == "${args[${#args[@]}-1]}" ]] && args_str=""
fi
done
domain="${args[0]:-}" key="${args[1]:-}" type="${args[2]:-}" expected_value="${args[3]:-}"

[ -z "$domain" ] || [ -z "$key" ] && continue

Expand Down
8 changes: 7 additions & 1 deletion lib/profile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ profile_load() {
fi
;;
modules)
# Split on commas, trim each value
# Split on commas, trim each value, validate names (alphanumeric + hyphens only)
MBP_PROFILE_MODULES=$(echo "$raw_val" | tr ',' '\n' | \
sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '\n' ' ' | sed 's/ $//')
for _mod in $MBP_PROFILE_MODULES; do
if ! echo "$_mod" | grep -qE '^[a-zA-Z0-9_-]+$'; then
printf "profile: invalid module name '%s' — only alphanumeric, hyphens, underscores allowed\n" "$_mod" >&2
return 1
fi
done
;;
brewfiles)
MBP_PROFILE_BREWFILES=$(echo "$raw_val" | tr ',' '\n' | \
Expand Down
27 changes: 20 additions & 7 deletions lib/state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ MBP_STATE_TXT="${MBP_STATE_DIR}/state.txt"
MBP_STATE_SCHEMA_VERSION=1

mkdir -p "$MBP_STATE_DIR"
chmod 700 "$MBP_STATE_DIR"

# Track temp files for cleanup on exit
_MBP_STATE_TMPFILES=()
_mbp_state_cleanup() { rm -f "${_MBP_STATE_TMPFILES[@]}" 2>/dev/null; }
trap _mbp_state_cleanup EXIT

# Safe mktemp that registers for cleanup
_mbp_mktemp() {
local f; f=$(mktemp)
_MBP_STATE_TMPFILES+=("$f")
echo "$f"
}

# === Plain-text state (modules 01-02) ===
# Format: module=status:exit_code:timestamp
Expand Down Expand Up @@ -93,7 +106,7 @@ state_migrate_from_txt() {
extra=$(jq -n --argjson count "$pkg_count" '{"packages": $count}')
fi

local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
jq --arg module "$module" \
--arg status "$status" \
--argjson exit_code "${exit_code:-0}" \
Expand All @@ -113,7 +126,7 @@ state_set_module_ok() {
command -v jq >/dev/null 2>&1 || return 1
[ -f "$MBP_STATE_JSON" ] || state_init_json
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
jq --arg module "$module" --arg ts "$ts" \
'.modules[$module] = (.modules[$module] // {} | . + {status: "ok", exit_code: 0, ran_at: $ts})' \
"$MBP_STATE_JSON" > "$tmp" && mv "$tmp" "$MBP_STATE_JSON"
Expand All @@ -124,7 +137,7 @@ state_set_module_error() {
command -v jq >/dev/null 2>&1 || return 1
[ -f "$MBP_STATE_JSON" ] || state_init_json
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
jq --arg module "$module" --arg ts "$ts" \
--argjson exit_code "$exit_code" --arg error "$error_msg" \
'.modules[$module] = (.modules[$module] // {} | . + {status: "error", exit_code: $exit_code, ran_at: $ts, error: $error})' \
Expand All @@ -135,7 +148,7 @@ state_set_module_meta() {
local module="$1" key="$2" value="$3"
command -v jq >/dev/null 2>&1 || return 1
[ -f "$MBP_STATE_JSON" ] || return 1
local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
# value may be a JSON fragment (array, number) or a plain string
if echo "$value" | jq . >/dev/null 2>&1; then
jq --arg module "$module" --arg key "$key" --argjson val "$value" \
Expand Down Expand Up @@ -165,15 +178,15 @@ state_set_profile() {
local profile="$1"
command -v jq >/dev/null 2>&1 || return 1
[ -f "$MBP_STATE_JSON" ] || state_init_json "$profile"
local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
jq --arg profile "$profile" '.profile = $profile' "$MBP_STATE_JSON" > "$tmp" && mv "$tmp" "$MBP_STATE_JSON"
}

state_set_last_run() {
command -v jq >/dev/null 2>&1 || return 1
[ -f "$MBP_STATE_JSON" ] || return 1
local ts; ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
jq --arg ts "$ts" '.last_run = $ts' "$MBP_STATE_JSON" > "$tmp" && mv "$tmp" "$MBP_STATE_JSON"
}

Expand Down Expand Up @@ -207,7 +220,7 @@ state_check_schema() {
printf "state: schema v%s detected, expected v%s — migrating...\n" \
"$current_schema" "$MBP_STATE_SCHEMA_VERSION"
# Future migration functions go here
local tmp; tmp=$(mktemp)
local tmp; tmp=$(_mbp_mktemp)
jq --argjson v "$MBP_STATE_SCHEMA_VERSION" '.schema_version = $v' \
"$MBP_STATE_JSON" > "$tmp" && mv "$tmp" "$MBP_STATE_JSON"
fi
Expand Down
4 changes: 3 additions & 1 deletion modules/01-xcode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ until xcode-select -p >/dev/null 2>&1; do
done

# Accept license
sudo xcodebuild -license accept 2>/dev/null || true
if ! sudo xcodebuild -license accept 2>/dev/null; then
mbp_log_warn "Could not auto-accept Xcode license — you may need to run: sudo xcodebuild -license accept"
fi

mbp_log_ok "Xcode Command Line Tools installed: $(xcode-select -p)"
state_txt_set "xcode" "ok" "0"
6 changes: 4 additions & 2 deletions modules/02-homebrew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ for bf in $BREWFILES; do
BFPATH="$BREWFILE_DIR/Brewfile.$bf"
if [ -f "$BFPATH" ]; then
mbp_log_step "Bundling: Brewfile.$bf"
brew bundle --file="$BFPATH" --no-upgrade 2>&1 | \
grep -v "^Using " | grep -v "^Homebrew Bundle complete" || true
if ! brew bundle --file="$BFPATH" --no-upgrade 2>&1 | \
grep -v "^Using " | grep -v "^Homebrew Bundle complete"; then
mbp_log_warn "brew bundle returned non-zero for Brewfile.$bf — some packages may have failed"
fi
else
mbp_log_warn "Brewfile.$bf not found, skipping"
fi
Expand Down
5 changes: 3 additions & 2 deletions modules/03-shell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ fi
# Ensure Homebrew zsh is in /etc/shells
ZSH_BIN="$(brew --prefix 2>/dev/null)/bin/zsh"
if [ -f "$ZSH_BIN" ]; then
if ! grep -qF "$ZSH_BIN" /etc/shells 2>/dev/null; then
# Validate the path contains no newlines or special characters before writing to /etc/shells
if [[ "$ZSH_BIN" =~ ^[a-zA-Z0-9/_.-]+$ ]] && ! grep -qF "$ZSH_BIN" /etc/shells 2>/dev/null; then
mbp_log_step "Adding Homebrew zsh to /etc/shells..."
echo "$ZSH_BIN" | sudo tee -a /etc/shells >/dev/null
printf '%s\n' "$ZSH_BIN" | sudo tee -a /etc/shells >/dev/null
fi

# Change default shell if needed
Expand Down
6 changes: 4 additions & 2 deletions modules/04-mise.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ for tool_spec in $MISE_TOOLS; do
tool_name="${tool_spec%%@*}"
tool_version="${tool_spec#*@}"
mbp_log_step "Ensuring ${tool_name}@${tool_version}..."
mise install "${tool_spec}" 2>&1 | grep -v "^mise " | tail -3 | while IFS= read -r line; do
if ! mise install "${tool_spec}" 2>&1 | grep -v "^mise " | tail -3 | while IFS= read -r line; do
[ -n "$line" ] && mbp_log_step "$line"
done
done; then
mbp_log_warn "mise install ${tool_spec} may have failed"
fi
mise use --global "${tool_spec}" 2>/dev/null || true
done

Expand Down
15 changes: 10 additions & 5 deletions modules/05-dotfiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@ link_dotfile() {
return
fi

# Backup existing non-symlink file
# Ensure parent directory exists
mkdir -p "$(dirname "$dst")"

# Backup existing regular file atomically (copy then link in one step)
if [ -f "$dst" ] && [ ! -L "$dst" ]; then
mkdir -p "$BACKUP_DIR"
cp "$dst" "$BACKUP_DIR/$(basename "$dst")"
mbp_log_step "backed up: ~/${dst_name} → $BACKUP_DIR"
fi

# Ensure parent directory exists
mkdir -p "$(dirname "$dst")"

ln -sf "$src" "$dst"
# Atomic symlink: create temp link then rename over destination
local tmp_link
tmp_link=$(mktemp "${dst}.mbp.XXXXXX")
rm -f "$tmp_link"
ln -s "$src" "$tmp_link"
mv -f "$tmp_link" "$dst"
mbp_log_ok "linked: ~/${dst_name}"
LINKED=$((LINKED + 1))
}
Expand Down
22 changes: 20 additions & 2 deletions modules/07-ssh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ for key in "$SSH_DIR"/*; do
[[ "$(basename "$key")" == "config"* ]] && continue
[[ "$(basename "$key")" == "environment" ]] && continue

chmod 600 "$key"
if ! chmod 600 "$key"; then
mbp_log_warn "failed to set permissions on $(basename "$key")"
fi
KEY_COUNT=$((KEY_COUNT + 1))
mbp_log_step "permissions 600: $(basename "$key")"

Expand All @@ -36,6 +38,22 @@ for key in "$SSH_DIR"/*; do
esac
done

# Warn about weak key types
if [ -n "$GITHUB_KEY" ] && [ -f "${GITHUB_KEY}.pub" ]; then
KEY_TYPE=$(ssh-keygen -l -f "${GITHUB_KEY}.pub" 2>/dev/null | awk '{print $4}' | tr -d '()')
KEY_BITS=$(ssh-keygen -l -f "${GITHUB_KEY}.pub" 2>/dev/null | awk '{print $1}')
case "$KEY_TYPE" in
DSA)
mbp_log_warn "SSH key $(basename "$GITHUB_KEY") uses DSA which is deprecated — generate an ed25519 key"
;;
RSA)
if [ "${KEY_BITS:-0}" -lt 3072 ]; then
mbp_log_warn "SSH key $(basename "$GITHUB_KEY") is RSA ${KEY_BITS}-bit — consider ed25519 or RSA 4096"
fi
;;
esac
fi

# Ensure config.d directory for per-client/per-project includes
mkdir -p "${SSH_DIR}/config.d"

Expand All @@ -47,7 +65,7 @@ if [ -n "$GITHUB_KEY" ]; then
Host github.com
HostName github.com
User git
IdentityFile ${GITHUB_KEY}
IdentityFile "${GITHUB_KEY}"
EOF
mbp_log_ok "GitHub SSH: using $(basename "$GITHUB_KEY")"
elif [ ! -f "$GITHUB_CONF" ]; then
Expand Down
2 changes: 1 addition & 1 deletion modules/10-ai-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ else
else
mbp_log_warn "npm not found — install node (module 04) and re-run"
state_set_module_error "ai-tools" "1" "npm not available for Claude Code install"
exit 1
return 1
fi
fi

Expand Down
7 changes: 7 additions & 0 deletions modules/11-macos-defaults.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ mbp_log_step "Activity Monitor..."
apply_default "com.apple.ActivityMonitor" "OpenMainWindow" "bool" "true"
apply_default "com.apple.ActivityMonitor" "ShowCategory" "int" "0"

mbp_log_step "Security..."
# Enable macOS firewall
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on 2>/dev/null || true
# Require password immediately after sleep/screen saver
apply_default "com.apple.screensaver" "askForPassword" "int" "1"
apply_default "com.apple.screensaver" "askForPasswordDelay" "int" "0"

# Restart affected system processes
mbp_log_step "Restarting system services..."
killall Dock 2>/dev/null || true
Expand Down
2 changes: 1 addition & 1 deletion profiles/devizer-full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
format = 1
modules = xcode,homebrew,shell,mise,dotfiles,git,ssh,secrets,docker,ai-tools,macos-defaults,apps,dev-dirs
brewfiles = core,dev,ai,apps,personal
mise_tools = node@22,ruby@3.3,python@3.12,bun@latest
mise_tools = node@22,ruby@3.3,python@3.12,bun@1.2
2 changes: 1 addition & 1 deletion profiles/personal.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
format = 1
modules = xcode,homebrew,shell,mise,dotfiles,git,ssh,secrets,docker,ai-tools,macos-defaults,apps,dev-dirs
brewfiles = core,dev,ai,apps,personal
mise_tools = node@22,ruby@3.3,python@3.12,bun@latest
mise_tools = node@22,ruby@3.3,python@3.12,bun@1.2
Loading