Metadata-Version: 2.4
Name: macstrap
Version: 1.2.0
Summary: Remote Mac setup and package management via SSH
Project-URL: Homepage, https://github.com/changyy/py-macstrap
Project-URL: Repository, https://github.com/changyy/py-macstrap
Project-URL: Bug Tracker, https://github.com/changyy/py-macstrap/issues
Author-email: changyy <changyy.csie@gmail.com>
License: MIT
Keywords: ansible,devops,homebrew,mac,macos,macports,setup
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Installation/Setup
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Requires-Dist: ansible-core>=2.16
Requires-Dist: click>=8.1
Requires-Dist: rich>=13.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# macstrap

[![PyPI](https://img.shields.io/pypi/v/macstrap.svg)](https://pypi.org/project/macstrap/)
[![PyPI Downloads](https://static.pepy.tech/badge/macstrap)](https://pepy.tech/projects/macstrap)

A CLI tool for setting up and managing Macs — no Ansible knowledge required.

Define what you want installed in plain text files, then run a single command to apply the full setup. Works both **locally** (on the Mac itself) and **remotely** (over SSH from another machine). Safe to re-run anytime; already-installed items are skipped.

---

## Quick Start: Fresh Mac Mini

Got a brand-new Mac mini? Here's the fastest path from unboxing to fully configured:

### Step 0: Get Python on a fresh Mac

A brand-new macOS has **no Python, no git, no compilers**. You need developer tools first.

**Option A — Install Xcode from the App Store (recommended)**

1. Open the **App Store** on the Mac
2. Search for **Xcode** and install it (~30 GB, takes a while)
3. After installation, open Terminal and run:

```bash
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
python3 --version   # should show Python 3.x
```

**Option B — Install Command Line Tools only (smaller)**

```bash
xcode-select --install
# A dialog will appear — click "Install" and wait
```

> **Note:** `xcode-select --install` requires a GUI dialog. If your Mac mini is headless (no display), use the App Store method via Screen Sharing, or install via `softwareupdate`:
>
> ```bash
> sudo touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
> LABEL=$(softwareupdate -l 2>&1 | grep -o "Label: Command Line Tools.*" | sort -r | head -1 | sed 's/Label: //')
> sudo softwareupdate -i "$LABEL" --verbose
> sudo rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
> ```

### Step 1: Install Homebrew + macstrap

macstrap requires **Python 3.10+**, but Xcode CLT only provides Python 3.9. Install Homebrew to get a newer Python:

```bash
# Install Homebrew (includes Python 3.12)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/opt/homebrew/bin/brew shellenv)"

# Install macstrap
pip3 install macstrap
```

### Step 2: Local setup on the Mac

```bash
macstrap local enable-ssh
#  ✓  Remote Login (SSH) enabled.
#    This Mac is reachable at:  mac-mini.local
#    SSH user:                  user
```

### Step 3: Remote setup from another machine

```bash
pip install macstrap
macstrap init                          # create package + settings files
macstrap ssh-auth mac-mini.local       # register + store credentials
macstrap run mac-mini.local            # apply full setup
```

### Prerequisites summary

| What | Why |
|------|-----|
| Xcode or Xcode CLT | Provides git, compilers, and Python 3.9 |
| Homebrew or MacPorts | Provides Python 3.10+ (required by macstrap) |
| `pip3 install macstrap` | Installs macstrap + Ansible |

---

## Local Commands

Run these **on the Mac itself** for initial machine setup:

### `macstrap local enable-ssh`

Enable the macOS SSH server (Remote Login) so other machines can connect:

```
% macstrap local enable-ssh
✓  Remote Login (SSH) enabled.

  This Mac is reachable at:  mac-mini.local
  SSH user:                  user

  Test from another machine:
    ssh user@mac-mini.local
```

### `macstrap local install-xcode`

Install Xcode Command Line Tools (git, compilers, Python, etc.):

```
% macstrap local install-xcode
✓  Xcode Command Line Tools already installed (v16.2.0.0.1.1733547573).
```

If not yet installed, macstrap uses `softwareupdate` to install non-interactively.

### `macstrap local install-brew`

Install Homebrew package manager:

```
% macstrap local install-brew
✓  Homebrew 5.0.13 is already installed.
```

Automatically adds `brew shellenv` to `~/.zprofile` and `~/.zshrc`.

### `macstrap local install-macports`

Install MacPorts package manager (auto-detects macOS version):

```
% macstrap local install-macports
✓  MacPorts is already installed (Version: 2.11.6).
```

Downloads the correct `.pkg` from GitHub releases for your macOS version.

---

## Remote Commands

Run these **from another machine** to manage Macs over SSH.

### 1. Initialise config files

```
% macstrap init
% macstrap init --examples
```

This creates a `settings.toml` and package list files:

```
settings.toml               # enable/disable roles (see below)
packages-macports.txt       # MacPorts packages
packages-brew.txt           # Homebrew formulae
packages-brew-casks.txt     # Homebrew casks (GUI apps)
packages-npm-global.txt     # Global npm packages (installed via nvm)
packages-pip-global.txt     # Global pip packages
```

Edit the package files like a shopping list — one package per line, `#` for comments:

```
# packages-brew.txt
git
gh
fzf
ripgrep
```

Use `--examples` to also create starter config directories under `examples/` (such as `ai-cli`, `openclaw`, `utilities-dev`, `php8.3-dev`).

---

### 2. Configure roles via `settings.toml`

Control which setup roles are enabled:

```toml
# settings.toml
[roles]
macports = false
homebrew = true
nvm = false
openjdk = false
docker = false
shell = true
pip = true
```

| Role | Default | What it does |
|------|:-------:|-------------|
| `macports` | off | Installs MacPorts and packages from `packages-macports.txt` |
| `homebrew` | **on** | Installs Homebrew formulae from `packages-brew.txt` and casks from `packages-brew-casks.txt` |
| `nvm` | off | Installs nvm, Node.js (v22), and global npm packages from `packages-npm-global.txt` |
| `openjdk` | off | Configures Java symlink/PATH for Homebrew OpenJDK |
| `docker` | off | Checks Docker Desktop status (install via `packages-brew-casks.txt`) |
| `shell` | **on** | Deploys unified `~/.zshrc` with PATH entries for enabled tools |
| `pip` | **on** | Installs global pip packages from `packages-pip-global.txt` |

Only enabled roles run during `macstrap run`. If `settings.toml` is missing, defaults apply.

---

### 3. Register a host

`ssh-auth` does two things:

1. **Copy your SSH public key** to the remote Mac (one-time, requires SSH password)
2. **Store the sudo password** in the system credential store

```
% macstrap ssh-auth mac-mini.local
% macstrap ssh-auth mac-mini.local --user remoteuser
% macstrap ssh-auth 192.168.1.100 --user remoteuser
```

If SSH key auth is already set up, skip the key copy:

```
% macstrap ssh-auth mac-mini.local --skip-key-copy
```

You can register multiple machines — each gets its own credential entry.

---

### 4. Run setup

```
% macstrap run
% macstrap run mac-mini.local
% macstrap run 192.168.1.100
% macstrap run remoteuser@mac-mini.local
% macstrap run --config examples/ai-cli --config examples/openclaw 192.168.1.100
```

macstrap connects over SSH, reads your `settings.toml` and package files, and installs everything that isn't already there. First run takes 30-60 minutes (MacPorts compiles from source). Subsequent runs take about 1 minute.

**SSH user resolution order** (highest priority first):

1. `--user` flag
2. `user@host` inline syntax
3. SSH user stored at `ssh-auth` time
4. Local OS username (fallback)

---

### 5. Verify installation

Check whether every package in your files is installed on the remote Mac:

```
% macstrap verify
% macstrap verify mac-mini.local
% macstrap verify 192.168.1.100 --user remoteuser
```

Output example:

```
  Tools
  ✓  brew       Homebrew 4.4.12
  ✓  port       Version: 2.12.3
  ✓  nvm        0.40.1
  ✓  node       v22.14.0
  ✗  docker     not found
  ✓  java       openjdk version "21.0.7" 2025-04-15
  ✓  python3    Python 3.13.2

  Homebrew (2/3)
  ✓  git
  ✓  ripgrep
  ✗  htop  (missing)

  ⚠  2 installed, 1 missing
```

Exits with code `0` if everything is present, `1` if anything is missing — useful in CI or scripts.

---

### Run only part of the setup

Use `--tag` to apply a single role:

```
% macstrap run --tag homebrew
% macstrap run mac-mini.local --tag macports
% macstrap run --tag nvm
% macstrap run --tag shell
% macstrap run --tag pip
```

Available tags: `macports` `homebrew` `nvm` `npm` `docker` `openjdk` `shell` `pip`

> **Note:** `--tag` selects which roles to consider, but `settings.toml` is the master switch. A role disabled in settings will be skipped even if selected by `--tag`.

### Dry run

Preview what would change without applying:

```
% macstrap run --check
% macstrap run mac-mini.local --check
```

---

### Manage registered hosts

```
% macstrap list

  macstrap credential store  (macOS Keychain)
  Default target: mac-mini.local

  Host              User          Password
  mac-mini.local    remoteuser    ✓ stored   ← default
  192.168.1.200     admin         ✓ stored
```

```
% macstrap delete mac-mini.local
% macstrap delete-all
```

---

## Example: install OpenJDK + Docker Desktop

```bash
macstrap init

# Enable the roles in settings.toml
# [roles]
# homebrew = true
# openjdk = true
# docker = true

echo "openjdk" >> packages-brew.txt
echo "docker" >> packages-brew-casks.txt
macstrap ssh-auth mac-mini.local
macstrap run mac-mini.local
macstrap verify mac-mini.local
```

After Docker Desktop is installed, sign in to the target Mac desktop and launch Docker once to accept the licence and complete first-run setup.

---

## What gets installed

macstrap first bootstraps the target Mac over raw SSH (installing Xcode CLT if missing), then runs enabled Ansible roles:

| Phase | Role | What it does |
|-------|------|-------------|
| Bootstrap | *(raw SSH)* | Installs Xcode Command Line Tools via `softwareupdate` if missing |
| Main | `macports` | Installs MacPorts and packages from `packages-macports.txt` |
| Main | `homebrew` | Installs Homebrew formulae and casks |
| Main | `nvm` | Installs nvm, Node.js, and global npm packages |
| Main | `docker` | Checks Docker Desktop status |
| Main | `openjdk` | Configures Java symlink/PATH for Homebrew OpenJDK |
| Main | `shell` | Deploys unified `~/.zshrc` with PATH entries for all tools |
| Main | `pip_global` | Installs global pip packages |

All roles are idempotent — re-running only installs what is missing. Each role is controlled by `settings.toml` and only runs when enabled.

### Fresh Mac note

On a brand-new Mac, macOS intercepts `/usr/bin/python3` and shows an installation dialog instead of running Python. macstrap handles this automatically over SSH: the bootstrap phase uses raw SSH commands (no Python needed) to install Xcode CLT first, then hands off to the normal Ansible playbook.

---

## Credential storage

| Platform | Storage location |
|----------|-----------------|
| macOS | Keychain (`security` command) — visible in Keychain Access |
| Linux | `~/.config/macstrap/` directory (chmod 600) |

Key naming convention:

```
macstrap-target          → current default host
macstrap-hosts           → comma-separated index of all registered hosts
macstrap-pass-{host}     → sudo password for a specific host
macstrap-user-{host}     → SSH username for a specific host
```

---

## Requirements

- Python 3.10+
- SSH access to the target Mac (key-based auth is set up automatically by `ssh-auth`)
- macOS 13+ on the target machine

Ansible is bundled as a dependency — `pip install macstrap` is all you need.

---

## License

MIT © [changyy](https://github.com/changyy)
