initialize project

This commit is contained in:
2026-01-03 15:47:49 +08:00
commit 66a5179c83
4 changed files with 496 additions and 0 deletions

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# MacOS Setup Wizard
A simple setup wizard helping me setup my laptop correctly and fast.

345
main.py Normal file
View File

@@ -0,0 +1,345 @@
#!/usr/bin/env python3
"""
macOS Setup Wizard - Automatisierte Installation und Konfiguration
Erstellt Verzeichnisse und installiert Apps aus einer JSON-Repository-Liste
"""
import os
import sys
import json
import subprocess
import urllib.request
from pathlib import Path
from typing import List, Dict
class Colors:
"""ANSI Farbcodes für Terminal-Ausgabe"""
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
END = '\033[0m'
BOLD = '\033[1m'
class MacSetupWizard:
def __init__(self):
self.directories: List[str] = [
'~/Development',
'~/Development/Projects',
'~/Development/Tools',
'~/Documents/Work'
]
self.apps: List[Dict[str, str]] = []
self.selected_apps: List[str] = []
def clear_screen(self):
"""Terminal-Bildschirm leeren"""
os.system('clear' if os.name != 'nt' else 'cls')
def print_header(self, text: str):
"""Formatierte Überschrift ausgeben"""
print(f"\n{Colors.BOLD}{Colors.CYAN}{'='*60}{Colors.END}")
print(f"{Colors.BOLD}{Colors.CYAN}{text.center(60)}{Colors.END}")
print(f"{Colors.BOLD}{Colors.CYAN}{'='*60}{Colors.END}\n")
def print_step(self, current: int, total: int, title: str):
"""Aktuellen Schritt anzeigen"""
print(f"{Colors.BLUE}[Schritt {current}/{total}]{Colors.END} {Colors.BOLD}{title}{Colors.END}\n")
def step_directories(self):
"""Schritt 1: Verzeichnisse verwalten und erstellen"""
self.clear_screen()
self.print_header("macOS Setup Wizard")
self.print_step(1, 4, "Verzeichnisse erstellen")
while True:
print(f"{Colors.YELLOW}Aktuelle Verzeichnisse:{Colors.END}")
for idx, directory in enumerate(self.directories, 1):
expanded_path = os.path.expanduser(directory)
exists = "" if os.path.exists(expanded_path) else ""
print(f" {idx}. {exists} {directory}")
print(f"\n{Colors.CYAN}Optionen:{Colors.END}")
print(" [a] Verzeichnis hinzufügen")
print(" [r] Verzeichnis entfernen")
print(" [c] Verzeichnisse erstellen und weiter")
print(" [q] Beenden")
choice = input(f"\n{Colors.GREEN}Auswahl:{Colors.END} ").strip().lower()
if choice == 'a':
new_dir = input(f"{Colors.GREEN}Neuer Pfad (z.B. ~/Projects):{Colors.END} ").strip()
if new_dir:
self.directories.append(new_dir)
print(f"{Colors.GREEN}✓ Verzeichnis hinzugefügt{Colors.END}")
elif choice == 'r':
try:
idx = int(input(f"{Colors.YELLOW}Nummer zum Entfernen:{Colors.END} ")) - 1
if 0 <= idx < len(self.directories):
removed = self.directories.pop(idx)
print(f"{Colors.GREEN}'{removed}' entfernt{Colors.END}")
else:
print(f"{Colors.RED}✗ Ungültige Nummer{Colors.END}")
except ValueError:
print(f"{Colors.RED}✗ Bitte eine Zahl eingeben{Colors.END}")
elif choice == 'c':
self.create_directories()
input(f"\n{Colors.GREEN}Enter drücken um fortzufahren...{Colors.END}")
break
elif choice == 'q':
print(f"\n{Colors.YELLOW}Setup abgebrochen.{Colors.END}")
sys.exit(0)
self.clear_screen()
self.print_header("macOS Setup Wizard")
self.print_step(1, 4, "Verzeichnisse erstellen")
def create_directories(self):
"""Verzeichnisse tatsächlich erstellen"""
print(f"\n{Colors.YELLOW}Erstelle Verzeichnisse...{Colors.END}")
for directory in self.directories:
expanded_path = os.path.expanduser(directory)
try:
Path(expanded_path).mkdir(parents=True, exist_ok=True)
print(f"{Colors.GREEN}{Colors.END} {directory}")
except Exception as e:
print(f"{Colors.RED}{Colors.END} {directory} - Fehler: {e}")
def step_load_apps(self):
"""Schritt 2: Apps-Liste aus Repository laden"""
self.clear_screen()
self.print_header("macOS Setup Wizard")
self.print_step(2, 4, "Apps-Liste laden")
default_url = "https://raw.githubusercontent.com/username/repo/main/apps.json"
print(f"{Colors.YELLOW}Beispiel JSON-Format:{Colors.END}")
print(json.dumps({
"apps": [
{
"id": "vscode",
"name": "Visual Studio Code",
"url": "https://code.visualstudio.com/download",
"type": "dmg",
"install_command": "brew install --cask visual-studio-code"
}
]
}, indent=2))
print(f"\n{Colors.CYAN}Repository-URL:{Colors.END}")
repo_url = input(f"[{default_url}]: ").strip() or default_url
try:
print(f"\n{Colors.YELLOW}Lade Apps-Liste...{Colors.END}")
with urllib.request.urlopen(repo_url) as response:
data = json.loads(response.read().decode())
self.apps = data.get('apps', [])
print(f"{Colors.GREEN}{len(self.apps)} Apps geladen{Colors.END}")
except Exception as e:
print(f"{Colors.RED}✗ Fehler beim Laden: {e}{Colors.END}")
print(f"{Colors.YELLOW}Verwende Demo-Daten...{Colors.END}")
self.apps = self.get_demo_apps()
input(f"\n{Colors.GREEN}Enter drücken um fortzufahren...{Colors.END}")
def get_demo_apps(self) -> List[Dict[str, str]]:
"""Demo-Apps für Testzwecke"""
return [
{
"id": "homebrew",
"name": "Homebrew",
"url": "https://brew.sh",
"type": "script",
"install_command": '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
},
{
"id": "vscode",
"name": "Visual Studio Code",
"url": "https://code.visualstudio.com",
"type": "brew",
"install_command": "brew install --cask visual-studio-code"
},
{
"id": "git",
"name": "Git",
"url": "https://git-scm.com",
"type": "brew",
"install_command": "brew install git"
},
{
"id": "docker",
"name": "Docker Desktop",
"url": "https://www.docker.com/products/docker-desktop",
"type": "brew",
"install_command": "brew install --cask docker"
},
{
"id": "iterm2",
"name": "iTerm2",
"url": "https://iterm2.com",
"type": "brew",
"install_command": "brew install --cask iterm2"
}
]
def step_select_apps(self):
"""Schritt 3: Apps zur Installation auswählen"""
self.clear_screen()
self.print_header("macOS Setup Wizard")
self.print_step(3, 4, "Apps auswählen")
while True:
print(f"{Colors.YELLOW}Verfügbare Apps:{Colors.END}")
for idx, app in enumerate(self.apps, 1):
selected = "" if app['id'] in self.selected_apps else ""
print(f" {idx}. [{selected}] {app['name']} ({app['type']})")
print(f"\n{Colors.CYAN}Optionen:{Colors.END}")
print(" [Nummer] App auswählen/abwählen")
print(" [a] Alle auswählen")
print(" [n] Keine auswählen")
print(" [i] Info zu einer App")
print(" [c] Weiter zur Installation")
print(" [q] Beenden")
choice = input(f"\n{Colors.GREEN}Auswahl:{Colors.END} ").strip().lower()
if choice == 'a':
self.selected_apps = [app['id'] for app in self.apps]
print(f"{Colors.GREEN}✓ Alle Apps ausgewählt{Colors.END}")
elif choice == 'n':
self.selected_apps = []
print(f"{Colors.YELLOW}○ Alle Apps abgewählt{Colors.END}")
elif choice == 'i':
try:
idx = int(input(f"{Colors.YELLOW}App-Nummer für Info:{Colors.END} ")) - 1
if 0 <= idx < len(self.apps):
app = self.apps[idx]
print(f"\n{Colors.CYAN}{''*50}{Colors.END}")
print(f"{Colors.BOLD}Name:{Colors.END} {app['name']}")
print(f"{Colors.BOLD}URL:{Colors.END} {app['url']}")
print(f"{Colors.BOLD}Typ:{Colors.END} {app['type']}")
print(f"{Colors.BOLD}Befehl:{Colors.END} {app.get('install_command', 'N/A')}")
print(f"{Colors.CYAN}{''*50}{Colors.END}")
except ValueError:
print(f"{Colors.RED}✗ Ungültige Eingabe{Colors.END}")
elif choice == 'c':
if not self.selected_apps:
print(f"{Colors.RED}✗ Keine Apps ausgewählt!{Colors.END}")
else:
break
elif choice == 'q':
print(f"\n{Colors.YELLOW}Setup abgebrochen.{Colors.END}")
sys.exit(0)
else:
try:
idx = int(choice) - 1
if 0 <= idx < len(self.apps):
app_id = self.apps[idx]['id']
if app_id in self.selected_apps:
self.selected_apps.remove(app_id)
print(f"{Colors.YELLOW}○ App abgewählt{Colors.END}")
else:
self.selected_apps.append(app_id)
print(f"{Colors.GREEN}✓ App ausgewählt{Colors.END}")
except ValueError:
print(f"{Colors.RED}✗ Ungültige Eingabe{Colors.END}")
input(f"\n{Colors.GREEN}Enter drücken um fortzufahren...{Colors.END}")
self.clear_screen()
self.print_header("macOS Setup Wizard")
self.print_step(3, 4, "Apps auswählen")
def step_install_apps(self):
"""Schritt 4: Ausgewählte Apps installieren"""
self.clear_screen()
self.print_header("macOS Setup Wizard")
self.print_step(4, 4, "Apps installieren")
selected_app_objects = [app for app in self.apps if app['id'] in self.selected_apps]
print(f"{Colors.YELLOW}Installation von {len(selected_app_objects)} Apps...{Colors.END}\n")
for idx, app in enumerate(selected_app_objects, 1):
print(f"\n{Colors.CYAN}[{idx}/{len(selected_app_objects)}]{Colors.END} {Colors.BOLD}{app['name']}{Colors.END}")
install_cmd = app.get('install_command')
if not install_cmd:
print(f"{Colors.YELLOW} ⚠ Kein Installationsbefehl definiert{Colors.END}")
print(f"{Colors.BLUE} → Öffne URL: {app['url']}{Colors.END}")
continue
print(f"{Colors.BLUE} → Führe aus: {install_cmd}{Colors.END}")
try:
# Installationsbefehl ausführen
result = subprocess.run(
install_cmd,
shell=True,
capture_output=True,
text=True,
timeout=300
)
if result.returncode == 0:
print(f"{Colors.GREEN} ✓ Erfolgreich installiert{Colors.END}")
else:
print(f"{Colors.RED} ✗ Installation fehlgeschlagen{Colors.END}")
if result.stderr:
print(f"{Colors.RED} Fehler: {result.stderr[:200]}{Colors.END}")
except subprocess.TimeoutExpired:
print(f"{Colors.RED} ✗ Timeout (>5 Minuten){Colors.END}")
except Exception as e:
print(f"{Colors.RED} ✗ Fehler: {e}{Colors.END}")
print(f"\n{Colors.GREEN}{Colors.BOLD}Installation abgeschlossen!{Colors.END}")
# Zusammenfassung
print(f"\n{Colors.CYAN}{''*60}{Colors.END}")
print(f"{Colors.BOLD}Zusammenfassung:{Colors.END}")
print(f"{len(self.directories)} Verzeichnisse erstellt")
print(f"{len(selected_app_objects)} Apps installiert")
print(f"{Colors.CYAN}{''*60}{Colors.END}\n")
def run(self):
"""Hauptablauf des Setup-Wizards"""
try:
self.step_directories()
self.step_load_apps()
self.step_select_apps()
self.step_install_apps()
print(f"\n{Colors.GREEN}{Colors.BOLD}✓ Setup erfolgreich abgeschlossen!{Colors.END}\n")
except KeyboardInterrupt:
print(f"\n\n{Colors.YELLOW}Setup durch Benutzer abgebrochen.{Colors.END}")
sys.exit(1)
except Exception as e:
print(f"\n{Colors.RED}Fehler: {e}{Colors.END}")
sys.exit(1)
def main():
"""Hauptfunktion"""
# Prüfen ob macOS
if sys.platform != 'darwin':
print(f"{Colors.YELLOW}Warnung: Dieses Script ist für macOS optimiert.{Colors.END}")
response = input("Trotzdem fortfahren? (j/n): ")
if response.lower() != 'j':
sys.exit(0)
wizard = MacSetupWizard()
wizard.run()
if __name__ == "__main__":
main()

133
poetry.lock generated Normal file
View File

@@ -0,0 +1,133 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "altgraph"
version = "0.17.5"
description = "Python graph (network) package"
optional = false
python-versions = "*"
files = [
{file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"},
{file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"},
]
[[package]]
name = "macholib"
version = "1.16.4"
description = "Mach-O header analysis and editing"
optional = false
python-versions = "*"
files = [
{file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"},
{file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"},
]
[package.dependencies]
altgraph = ">=0.17"
[[package]]
name = "packaging"
version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
name = "pefile"
version = "2024.8.26"
description = "Python PE parsing module"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"},
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
]
[[package]]
name = "pyinstaller"
version = "6.17.0"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false
python-versions = "<3.15,>=3.8"
files = [
{file = "pyinstaller-6.17.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4e446b8030c6e5a2f712e3f82011ecf6c7ead86008357b0d23a0ec4bcde31dac"},
{file = "pyinstaller-6.17.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa9fd87aaa28239c6f0d0210114029bd03f8cac316a90bab071a5092d7c85ad7"},
{file = "pyinstaller-6.17.0-py3-none-manylinux2014_i686.whl", hash = "sha256:060b122e43e7c0b23e759a4153be34bd70914135ab955bb18a67181e0dca85a2"},
{file = "pyinstaller-6.17.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cd213d1a545c97dfe4a3c40e8213ff7c5127fc115c49229f27a3fa541503444b"},
{file = "pyinstaller-6.17.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:89c0d18ba8b62c6607abd8cf2299ae5ffa5c36d8c47f39608ce8c3f357f6099f"},
{file = "pyinstaller-6.17.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2a147b83cdebb07855bd5a663600891550062373a2ca375c58eacead33741a27"},
{file = "pyinstaller-6.17.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:f8cfbbfa6708e54fb936df6dd6eafaf133e84efb0d2fe25b91cfeefa793c4ca4"},
{file = "pyinstaller-6.17.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:97f4c1942f7b4cd73f9e38b49cc8f5f8a6fbb44922cb60dd3073a189b77ee1ae"},
{file = "pyinstaller-6.17.0-py3-none-win32.whl", hash = "sha256:ce0be227a037fd4be672226db709088565484f597d6b230bceec19850fdd4c85"},
{file = "pyinstaller-6.17.0-py3-none-win_amd64.whl", hash = "sha256:b019940dbf7a01489d6b26f9fb97db74b504e0a757010f7ad078675befc85a82"},
{file = "pyinstaller-6.17.0-py3-none-win_arm64.whl", hash = "sha256:3c92a335e338170df7e615f75279cfeea97ade89e6dd7694943c8c185460f7b7"},
{file = "pyinstaller-6.17.0.tar.gz", hash = "sha256:be372bd911392b88277e510940ac32a5c2a6ce4b8d00a311c78fa443f4f27313"},
]
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2025.9"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
[package.extras]
completion = ["argcomplete"]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.11"
description = "Community maintained hooks for PyInstaller"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyinstaller_hooks_contrib-2025.11-py3-none-any.whl", hash = "sha256:777e163e2942474aa41a8e6d31ac1635292d63422c3646c176d584d04d971c34"},
{file = "pyinstaller_hooks_contrib-2025.11.tar.gz", hash = "sha256:dfe18632e06655fa88d218e0d768fd753e1886465c12a6d4bce04f1aaeec917d"},
]
[package.dependencies]
packaging = ">=22.0"
setuptools = ">=42.0.0"
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
optional = false
python-versions = ">=3.6"
files = [
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
]
[[package]]
name = "setuptools"
version = "80.9.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.13"
content-hash = "8b9049a2e56b8d2d81d5dd0fa6f03a029c468e7b3dfac6eaf5ddfa1fcaceaedb"

15
pyproject.toml Normal file
View File

@@ -0,0 +1,15 @@
[tool.poetry]
name = "mac-setup"
version = "0.1.0"
description = "My setup wizard for MacOS"
authors = ["Void <mail@voideuw.dev>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.13"
pyinstaller = { version = "*", python = "<3.15" }
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"