feat(provisioner): Aurelio workstation provisioner with OS-family detection and profiles

- Add .aurelio/provisioner/ package with OS family detection (debian, rhel, arch, macos, wsl)
- Define toolchain profiles: embedded, backend, frontend, hardware, full
- Generate self-contained shell scripts per OS family and profile
- Add unit tests for detection and rendering
- Integrate provisioner into savearth-workspace MCP server
- Support ?profile= query parameter on /bootstrap/{os} endpoint
- Update README and dashboard provisioning panel
This commit is contained in:
Fábio Cunha 2026-07-05 09:59:42 +01:00
parent 33c990b61e
commit a2e40dd556
16 changed files with 1206 additions and 204 deletions

View file

@ -42,10 +42,14 @@ The server can generate bootstrap scripts that prepare a fresh machine for savea
After cloning `aws-iot-core-poc`, run:
```bash
# Embedded/firmware profile (default)
bash scripts/bootstrap-workstation.sh my-laptop
# Or pick a different toolchain profile
bash scripts/bootstrap-workstation.sh --profile backend my-laptop
```
This installs dependencies, clones the other savearth repos, initializes submodules, and registers the machine.
This detects your OS family (Debian, RHEL/Fedora, Arch, macOS, WSL), installs the right dependencies, clones the other savearth repos, initializes submodules, and registers the machine.
### One-click bootstrap from the dashboard
@ -56,15 +60,19 @@ Generate a pre-authorized bootstrap script via the MCP tool:
"name": "generate_bootstrap_script",
"arguments": {
"machine_name": "my-laptop",
"os_type": "fedora"
"os_type": "fedora",
"profile": "embedded"
}
}
```
Supported `os_type` values: `fedora`, `rhel`, `debian`, `ubuntu`, `arch`, `macos`, `wsl`.
Supported `profile` values: `embedded`, `backend`, `frontend`, `hardware`, `full`.
Or download directly:
```bash
curl -fsSL https://savearth-workspace.portugalfuturista.org/bootstrap/fedora?machine=my-laptop -o bootstrap.sh
curl -fsSL "https://savearth-workspace.portugalfuturista.org/bootstrap/fedora?machine=my-laptop&profile=embedded" -o bootstrap.sh
bash bootstrap.sh
```

View file

@ -41,6 +41,30 @@ from starlette.routing import Mount, Route
_THIS_DIR = Path(__file__).resolve().parent
# Allow importing the Aurelio provisioner both from repo and deployed layouts.
_PROVISIONER_SEARCH_PATHS = [
_THIS_DIR / "provisioner",
_THIS_DIR.parent.parent / "provisioner",
]
for _pp in _PROVISIONER_SEARCH_PATHS:
if _pp.exists() and str(_pp.parent) not in sys.path:
sys.path.insert(0, str(_pp.parent))
break
try:
from provisioner import (
OsFamily,
ProvisionerError,
VALID_PROFILES,
detect_os,
render_bootstrap_script,
)
except ImportError as _provisioner_import_err: # pragma: no cover
raise RuntimeError(
"Could not import Aurelio provisioner. Ensure the provisioner package is "
"available next to server.py or at replica-omnisciente/.aurelio/provisioner."
) from _provisioner_import_err
SAVEARTH_PROJECT_DIR = Path(os.environ.get("SAVEARTH_PROJECT_DIR", "/opt/savearth"))
SAVEARTH_MCP_URL = os.environ.get("SAVEARTH_MCP_URL", "http://192.168.0.212:8080/sse")
SAVEARTH_LOGO_URL = os.environ.get(
@ -310,200 +334,42 @@ def _collect_tool_versions() -> Dict[str, Any]:
return versions
def _render_bootstrap_script(os_type: str, machine_name: str, token: str) -> str:
"""Render a bootstrap script for the requested OS."""
machine_id = _machine_id(machine_name)
public_url = SAVEARTH_WORKSPACE_PUBLIC_URL.rstrip("/")
def _render_bootstrap_script(os_type: str, machine_name: str, token: str, profile: str = "embedded") -> str:
"""Render a bootstrap script using the Aurelio provisioner.
if os_type == "fedora":
return f"""#!/usr/bin/env bash
# savearth workstation bootstrap for Fedora
# Machine: {machine_name}
# Generated: {_now()}
set -euo pipefail
os_type maps to OS family: debian, rhel, arch, macos, wsl, fedora (treated as rhel).
"""
from provisioner.detect import OsFamily, OsInfo
MACHINE_NAME="{machine_name}"
MACHINE_ID="{machine_id}"
TOKEN="{token}"
PUBLIC_URL="{public_url}"
SAVEARTH_DIR="${{SAVEARTH_DIR:-$HOME/savearth}}"
ESP_IDF_VERSION="v5.5.4"
os_type = os_type.lower()
family_map = {
"fedora": OsFamily.RHEL,
"rhel": OsFamily.RHEL,
"rocky": OsFamily.RHEL,
"debian": OsFamily.DEBIAN,
"ubuntu": OsFamily.DEBIAN,
"arch": OsFamily.ARCH,
"macos": OsFamily.MACOS,
"darwin": OsFamily.MACOS,
"wsl": OsFamily.WSL,
}
family = family_map.get(os_type)
if family is None:
# Fall back to auto-detection if the caller passes an unknown string.
info = detect_os()
family = info.family
distro = info.distro
else:
distro = os_type
echo "🔧 Bootstrapping savearth workstation: $MACHINE_NAME"
# ─── 1. System packages ───────────────────────────────────────────────
echo "Installing base packages..."
sudo dnf update -y
sudo dnf install -y \\
git git-lfs curl wget python3 python3-pip python3-venv \\
cmake ninja-build ccache flex bison gperf \\
libusb1 dfu-util minicom picocom \\
udev docker docker-compose
sudo usermod -aG docker "$USER" || true
sudo systemctl enable --now docker || true
# ─── 2. Node.js / npx for MCP remote clients ──────────────────────────
if ! command -v npx &>/dev/null; then
echo "Installing Node.js..."
sudo dnf install -y nodejs
fi
# ─── 3. ESP-IDF ───────────────────────────────────────────────────────
mkdir -p "$HOME/esp"
if [ ! -d "$HOME/esp/v5.5.4/esp-idf" ]; then
echo "Cloning ESP-IDF $ESP_IDF_VERSION..."
git clone -b "$ESP_IDF_VERSION" --recursive \\
https://github.com/espressif/esp-idf.git "$HOME/esp/v5.5.4/esp-idf"
cd "$HOME/esp/v5.5.4/esp-idf"
./install.sh esp32s3
fi
echo 'export IDF_PATH="$HOME/esp/v5.5.4/esp-idf"' >> "$HOME/.bashrc"
echo 'source "$IDF_PATH/export.sh" > /dev/null 2>&1' >> "$HOME/.bashrc"
# ─── 4. GitHub SSH auth ───────────────────────────────────────────────
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
echo "Generating SSH key for GitHub..."
mkdir -p "$HOME/.ssh"
ssh-keygen -t ed25519 -C "$MACHINE_NAME@savearth" -f "$HOME/.ssh/id_ed25519" -N ""
echo "Add this key to GitHub:"
cat "$HOME/.ssh/id_ed25519.pub"
read -rp "Press Enter after adding the key to GitHub..."
fi
# ─── 5. Clone savearth repositories ───────────────────────────────────
mkdir -p "$SAVEARTH_DIR"
cd "$SAVEARTH_DIR"
clone_or_pull() {{
local repo="$1"
local dir="$2"
if [ -d "$dir/.git" ]; then
echo "Pulling $repo..."
git -C "$dir" pull
else
echo "Cloning $repo..."
git clone "git@github.com:$repo.git" "$dir"
fi
}}
clone_or_pull SavearthTech/aws-iot-core-poc aws-iot-core-poc
clone_or_pull SavearthTech/savearth-iot-infrastructure savearth-iot-infrastructure
clone_or_pull SavearthTech/savearth-hw-project savearth-hw-project
clone_or_pull SavearthTech/hardware-devicesFirmwareTest hardware-devicesFirmwareTest
# ─── 6. Initialize central-brain submodules ───────────────────────────
for dir in aws-iot-core-poc savearth-iot-infrastructure savearth-hw-project hardware-devicesFirmwareTest; do
echo "Initializing submodule in $dir..."
git -C "$dir" submodule update --init --recursive replica-omnisciente
done
# ─── 7. Register this machine with savearth-workspace ─────────────────
echo "Registering machine..."
curl -sS -X POST "$PUBLIC_URL/api/register" \\
-H "Content-Type: application/json" \\
-d '{{"machine_name":"$MACHINE_NAME","os":"fedora","token":"$TOKEN"}}' \\
-o /tmp/register.json || true
cat /tmp/register.json 2>/dev/null || echo "Registration skipped (server may require auth)."
# ─── 8. Pull latest brain snapshot if available ───────────────────────
echo "Checking for brain snapshots..."
curl -sS "$PUBLIC_URL/api/snapshots/latest?machine_id=$MACHINE_ID" \\
-H "Authorization: Bearer $TOKEN" \\
-o /tmp/latest_snapshot.json || true
if [ -s /tmp/latest_snapshot.json ]; then
SNAPSHOT_URL=$(python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('download_url',''))" < /tmp/latest_snapshot.json)
if [ -n "$SNAPSHOT_URL" ]; then
echo "Restoring brain snapshot..."
curl -sS -H "Authorization: Bearer $TOKEN" "$SNAPSHOT_URL" -o /tmp/brain.tar.gz
for dir in aws-iot-core-poc savearth-iot-infrastructure savearth-hw-project hardware-devicesFirmwareTest; do
[ -d "$dir/replica-omnisciente/.aurelio/brain" ] && \\
tar xzf /tmp/brain.tar.gz -C "$dir/replica-omnisciente/.aurelio" 2>/dev/null || true
done
fi
fi
# ─── 9. Global Aurelio config ─────────────────────────────────────────
mkdir -p "$HOME/.aurelio"
cat > "$HOME/.aurelio/config.toml" << 'TOML'
[sync]
enabled = true
endpoint = "https://mcp.portugalfuturista.org"
interval_seconds = 300
auto_push = true
[identity]
name = "savearth Developer Workstation"
realm = "smart-device-firmware"
[models]
default_local = "qwen2.5-coder:14b"
default_cloud = "gemini-2.5-pro"
ollama_url = "http://127.0.0.1:11434"
[brain]
auto_save = true
artifact_types = ["task", "implementation_plan", "walkthrough", "analysis"]
TOML
echo ""
echo "✅ Bootstrap complete. Next steps:"
echo " 1. Start a new shell or run: source ~/.bashrc"
echo " 2. cd $SAVEARTH_DIR/aws-iot-core-poc"
echo " 3. python3 tools/scripts/build_tool.py env"
echo " 4. Open dashboard: $PUBLIC_URL/dashboard"
"""
# Generic Linux fallback (similar to Fedora without dnf specifics)
return f"""#!/usr/bin/env bash
# savearth workstation bootstrap for {os_type}
# Machine: {machine_name}
# Generated: {_now()}
set -euo pipefail
MACHINE_NAME="{machine_name}"
MACHINE_ID="{machine_id}"
TOKEN="{token}"
PUBLIC_URL="{public_url}"
SAVEARTH_DIR="${{SAVEARTH_DIR:-$HOME/savearth}}"
echo "🔧 Bootstrapping savearth workstation: $MACHINE_NAME"
echo "OS: {os_type}"
echo ""
echo "Please ensure the following are installed manually:"
echo " - git, python3, python3-pip, python3-venv"
echo " - cmake, ninja-build, ccache"
echo " - docker, docker-compose"
echo " - ESP-IDF v5.5.4 at ~/esp/v5.5.4/esp-idf"
echo " - Node.js + npx"
echo ""
read -rp "Press Enter when ready..."
# Clone repositories
mkdir -p "$SAVEARTH_DIR"
cd "$SAVEARTH_DIR"
for repo in SavearthTech/aws-iot-core-poc:aws-iot-core-poc \\
SavearthTech/savearth-iot-infrastructure:savearth-iot-infrastructure \\
SavearthTech/savearth-hw-project:savearth-hw-project \\
SavearthTech/hardware-devicesFirmwareTest:hardware-devicesFirmwareTest; do
r="${{repo%%:*}}"
d="${{repo##*:}}"
if [ -d "$d/.git" ]; then
git -C "$d" pull
else
git clone "git@github.com:$r.git" "$d"
fi
git -C "$d" submodule update --init --recursive replica-omnisciente
done
# Register machine
curl -sS -X POST "$PUBLIC_URL/api/register" \\
-H "Content-Type: application/json" \\
-d '{{"machine_name":"$MACHINE_NAME","os":"{os_type}","token":"$TOKEN"}}' || true
echo "✅ Bootstrap complete. cd $SAVEARTH_DIR/aws-iot-core-poc"
"""
os_info = OsInfo(family=family, distro=distro, version="", is_wsl=family == OsFamily.WSL)
return render_bootstrap_script(
os_info=os_info,
machine_name=machine_name,
profile=profile,
token=token,
public_url=SAVEARTH_WORKSPACE_PUBLIC_URL,
)
# ─── Project Data Loading ────────────────────────────────────────────
@ -815,9 +681,10 @@ DASHBOARD_TEMPLATE = """<!DOCTYPE html>
</table>
{% else %}
<p>No workstations registered yet.</p>
<p>To provision a new Fedora laptop, run:</p>
<pre style="background:#0f172a;padding:1rem;border-radius:8px;overflow:auto;">curl -fsSL {{ public_url }}/bootstrap/fedora?machine=my-laptop -o bootstrap.sh
<p>Provision a new machine (auto-detects Debian, RHEL/Fedora, Arch, macOS, WSL):</p>
<pre style="background:#0f172a;padding:1rem;border-radius:8px;overflow:auto;">curl -fsSL "{{ public_url }}/bootstrap/fedora?machine=my-laptop&profile=embedded" -o bootstrap.sh
bash bootstrap.sh</pre>
<p>Available profiles: <code>embedded</code>, <code>backend</code>, <code>frontend</code>, <code>hardware</code>, <code>full</code>.</p>
{% endif %}
</div>
</div>
@ -962,17 +829,28 @@ async def get_dashboard_url() -> str:
name="generate_bootstrap_script",
annotations={"title": "Generate Workstation Bootstrap Script", "readOnlyHint": True},
)
async def generate_bootstrap_script(machine_name: str, os_type: str = "fedora") -> str:
"""Generate a bootstrap script for a new workstation. os_type: fedora or generic."""
async def generate_bootstrap_script(
machine_name: str,
os_type: str = "fedora",
profile: str = "embedded",
) -> str:
"""Generate a bootstrap script for a new workstation.
os_type: fedora, rhel, debian, ubuntu, arch, macos, wsl (default: fedora).
profile: embedded, backend, frontend, hardware, full (default: embedded).
"""
if not machine_name or not re.match(r"^[a-zA-Z0-9_-]+$", machine_name):
return "Invalid machine_name. Use only letters, numbers, hyphens, and underscores."
os_type = os_type.lower()
if os_type not in ("fedora", "generic"):
return "Unsupported os_type. Use 'fedora' or 'generic'."
profile = profile.lower()
if profile not in VALID_PROFILES:
return f"Unsupported profile '{profile}'. Valid: {', '.join(sorted(VALID_PROFILES))}."
token = _generate_token(machine_name)
script = _render_bootstrap_script(os_type, machine_name, token)
try:
script = _render_bootstrap_script(os_type, machine_name, token, profile)
except ProvisionerError as exc:
return f"Provisioning error: {exc}"
return (
f"# Bootstrap script for {machine_name} ({os_type})\n"
f"# Bootstrap script for {machine_name} ({os_type}, {profile})\n"
f"# Save this to bootstrap.sh and run: bash bootstrap.sh\n\n"
f"{script}"
)
@ -1208,12 +1086,21 @@ async def bootstrap_script_handler(request: Request) -> PlainTextResponse:
"""Public endpoint to download a bootstrap script for a machine."""
os_type = request.path_params.get("os", "fedora")
machine_name = request.query_params.get("machine")
profile = request.query_params.get("profile", "embedded")
if not machine_name:
return PlainTextResponse("Missing ?machine=<name>", status_code=400)
if not re.match(r"^[a-zA-Z0-9_-]+$", machine_name):
return PlainTextResponse("Invalid machine name", status_code=400)
if profile not in VALID_PROFILES:
return PlainTextResponse(
f"Invalid profile '{profile}'. Valid: {', '.join(sorted(VALID_PROFILES))}.",
status_code=400,
)
token = _generate_token(machine_name)
script = _render_bootstrap_script(os_type, machine_name, token)
try:
script = _render_bootstrap_script(os_type, machine_name, token, profile)
except ProvisionerError as exc:
return PlainTextResponse(f"Provisioning error: {exc}", status_code=400)
return PlainTextResponse(
script,
media_type="text/x-shellscript",

View file

@ -0,0 +1,25 @@
"""Aurelio workstation provisioner.
Generate self-contained shell scripts that prepare a developer workstation
for the savearth ecosystem. Supports multiple OS families and toolchain profiles.
"""
from .detect import OsFamily, OsInfo, detect_os, os_family_name
from .profiles import VALID_PROFILES, get_profile
from .renderer import (
ProvisionerError,
render_bootstrap_script,
render_for_current_machine,
)
__all__ = [
"OsFamily",
"OsInfo",
"VALID_PROFILES",
"ProvisionerError",
"detect_os",
"get_profile",
"os_family_name",
"render_bootstrap_script",
"render_for_current_machine",
]

View file

@ -0,0 +1,103 @@
"""OS-family detection for the Aurelio workstation provisioner."""
from __future__ import annotations
import platform
import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Optional
class OsFamily(str, Enum):
DEBIAN = "debian"
RHEL = "rhel"
ARCH = "arch"
MACOS = "macos"
WSL = "wsl"
UNKNOWN = "unknown"
@dataclass(frozen=True)
class OsInfo:
family: OsFamily
distro: str
version: str
is_wsl: bool
@property
def is_linux(self) -> bool:
return self.family in (OsFamily.DEBIAN, OsFamily.RHEL, OsFamily.ARCH)
def _read_os_release() -> dict[str, str]:
"""Parse /etc/os-release into a dict."""
data: dict[str, str] = {}
for path in (Path("/etc/os-release"), Path("/usr/lib/os-release")):
if path.exists():
for line in path.read_text(encoding="utf-8").splitlines():
if "=" in line:
key, value = line.split("=", 1)
data[key] = value.strip('"')
break
return data
def _is_wsl() -> bool:
"""Detect Windows Subsystem for Linux."""
if platform.system() != "Linux":
return False
proc_version = Path("/proc/version")
if proc_version.exists() and "microsoft" in proc_version.read_text(encoding="utf-8").lower():
return True
return Path("/mnt/c/Windows").exists()
def _map_linux_distro(os_release: dict[str, str]) -> OsFamily:
"""Map an /etc/os-release ID to our OS family."""
distro_id = os_release.get("ID", "").lower()
like = os_release.get("ID_LIKE", "").lower()
debian_ids = {"debian", "ubuntu", "linuxmint", "pop", "elementary", "zorin", "kali", "raspbian"}
rhel_ids = {"fedora", "rhel", "rocky", "almalinux", "centos", "ol", "amzn"}
arch_ids = {"arch", "manjaro", "endeavouros", "garuda"}
if distro_id in debian_ids or "debian" in like or "ubuntu" in like:
return OsFamily.DEBIAN
if distro_id in rhel_ids or "fedora" in like or "rhel" in like or "centos" in like:
return OsFamily.RHEL
if distro_id in arch_ids or "arch" in like:
return OsFamily.ARCH
return OsFamily.UNKNOWN
def detect_os() -> OsInfo:
"""Detect the workstation OS family and distribution."""
system = platform.system()
is_wsl = _is_wsl()
if system == "Darwin":
version = platform.mac_ver()[0]
return OsInfo(OsFamily.MACOS, "macos", version, is_wsl)
if system == "Linux":
os_release = _read_os_release()
family = _map_linux_distro(os_release)
distro = os_release.get("ID", "linux")
version = os_release.get("VERSION_ID", "")
if is_wsl:
return OsInfo(OsFamily.WSL, distro, version, True)
return OsInfo(family, distro, version, False)
if system == "Windows":
return OsInfo(OsFamily.UNKNOWN, "windows", platform.release(), False)
return OsInfo(OsFamily.UNKNOWN, system.lower(), platform.release(), False)
def os_family_name() -> str:
"""Convenience: return the detected OS family as a string."""
return detect_os().family.value

View file

@ -0,0 +1,209 @@
"""Package lists per OS family for the Aurelio workstation provisioner.
Keep these in sync with the container Dockerfiles in the firmware repo:
- tools/containers/debian/Dockerfile
- tools/containers/rockylinux/Dockerfile
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from .detect import OsFamily
@dataclass(frozen=True)
class PackageManager:
"""Describes how a given OS family installs packages."""
name: str
update_cmd: str
install_cmd: str
group_cmd: str | None = None
PACKAGE_MANAGERS: dict[OsFamily, PackageManager] = {
OsFamily.DEBIAN: PackageManager(
name="apt-get",
update_cmd="sudo apt-get update",
install_cmd="sudo apt-get install -y --no-install-recommends",
),
OsFamily.RHEL: PackageManager(
name="dnf",
update_cmd="sudo dnf update -y",
install_cmd="sudo dnf install -y",
group_cmd="sudo dnf group install -y",
),
OsFamily.ARCH: PackageManager(
name="pacman",
update_cmd="sudo pacman -Sy",
install_cmd="sudo pacman -S --noconfirm",
),
OsFamily.MACOS: PackageManager(
name="brew",
update_cmd="brew update",
install_cmd="brew install",
),
}
# Base packages required on every workstation.
BASE_PACKAGES: dict[OsFamily, list[str]] = {
OsFamily.DEBIAN: [
"git",
"git-lfs",
"curl",
"wget",
"ca-certificates",
"python3",
"python3-pip",
"python3-venv",
"python3-setuptools",
"cmake",
"ninja-build",
"ccache",
"flex",
"bison",
"gperf",
"libffi-dev",
"libssl-dev",
"dfu-util",
"libusb-1.0-0",
"unzip",
"xz-utils",
"minicom",
"picocom",
"udev",
],
OsFamily.RHEL: [
"git",
"wget",
"flex",
"bison",
"gperf",
"python3",
"python3-pip",
"python3-setuptools",
"python3-devel",
"cmake",
"ninja-build",
"ccache",
"libffi-devel",
"openssl-devel",
"ca-certificates",
"curl",
"unzip",
"xz",
"minicom",
"picocom",
"gcc",
"gcc-c++",
"make",
"libusb1",
"systemd-udev",
"dfu-util",
],
OsFamily.ARCH: [
"git",
"wget",
"flex",
"bison",
"gperf",
"python",
"python-pip",
"cmake",
"ninja",
"ccache",
"libffi",
"openssl",
"dfu-util",
"libusb",
"minicom",
"picocom",
"udev",
],
OsFamily.MACOS: [
"git",
"python",
"cmake",
"ninja",
"ccache",
"dfu-util",
"minicom",
],
}
# Profile-specific native packages.
PROFILE_PACKAGES: dict[str, dict[OsFamily, list[str]]] = {
"embedded": {
OsFamily.DEBIAN: [],
OsFamily.RHEL: [],
OsFamily.ARCH: [],
OsFamily.MACOS: [],
},
"backend": {
OsFamily.DEBIAN: ["nodejs", "npm"],
OsFamily.RHEL: ["nodejs", "npm"],
OsFamily.ARCH: ["nodejs", "npm"],
OsFamily.MACOS: ["node"],
},
"frontend": {
OsFamily.DEBIAN: ["nodejs", "npm"],
OsFamily.RHEL: ["nodejs", "npm"],
OsFamily.ARCH: ["nodejs", "npm"],
OsFamily.MACOS: ["node"],
},
"hardware": {
# KiCad and related EDA tooling (best-effort; users may need nightly/stable PPA).
OsFamily.DEBIAN: ["kicad"],
OsFamily.RHEL: [],
OsFamily.ARCH: ["kicad"],
OsFamily.MACOS: ["kicad"],
},
}
# Python packages installed via pip regardless of OS.
PYTHON_PACKAGES: dict[str, list[str]] = {
"embedded": ["pyserial", "esptool", "pytest", "pyyaml"],
"backend": ["boto3", "requests", "pyyaml"],
"frontend": [],
"hardware": [],
}
def packages_for_profile(family: OsFamily, profile: str) -> list[str]:
"""Return the native package list for a given OS family and profile."""
base = list(BASE_PACKAGES.get(family, []))
if profile == "full":
for prof in ("embedded", "backend", "frontend", "hardware"):
base.extend(PROFILE_PACKAGES.get(prof, {}).get(family, []))
# Remove duplicates while preserving order.
seen: set[str] = set()
deduped: list[str] = []
for pkg in base:
if pkg not in seen:
seen.add(pkg)
deduped.append(pkg)
return deduped
return base + PROFILE_PACKAGES.get(profile, {}).get(family, [])
def python_packages_for_profile(profile: str) -> list[str]:
"""Return pip packages for a profile (or all profiles for 'full')."""
if profile == "full":
seen: set[str] = set()
result: list[str] = []
for pkgs in PYTHON_PACKAGES.values():
for pkg in pkgs:
if pkg not in seen:
seen.add(pkg)
result.append(pkg)
return result
return list(PYTHON_PACKAGES.get(profile, []))
def package_manager_for(family: OsFamily) -> PackageManager | None:
return PACKAGE_MANAGERS.get(family)

View file

@ -0,0 +1,62 @@
"""Toolchain profile definitions for the Aurelio workstation provisioner."""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Profile:
"""A workstation provisioning profile."""
name: str
description: str
needs_esp_idf: bool = False
needs_docker: bool = False
needs_node: bool = False
needs_github_ssh: bool = True
clones_repos: bool = True
PROFILES: dict[str, Profile] = {
"embedded": Profile(
name="embedded",
description="Firmware development for ESP32-S3 (ESP-IDF, cross-compilation, flashing)",
needs_esp_idf=True,
needs_docker=True,
needs_node=True,
),
"backend": Profile(
name="backend",
description="IoT backend, AWS infrastructure, Lambdas, telemetry pipelines",
needs_docker=True,
needs_node=True,
),
"frontend": Profile(
name="frontend",
description="Web dashboards and frontend tooling",
needs_node=True,
),
"hardware": Profile(
name="hardware",
description="PCB design, KiCad, BOM management, EDA workflows",
needs_docker=False,
needs_node=False,
),
"full": Profile(
name="full",
description="Everything: embedded, backend, frontend, and hardware tooling",
needs_esp_idf=True,
needs_docker=True,
needs_node=True,
),
}
VALID_PROFILES = set(PROFILES.keys())
def get_profile(name: str) -> Profile:
if name not in PROFILES:
raise ValueError(f"Unknown profile '{name}'. Valid profiles: {', '.join(VALID_PROFILES)}")
return PROFILES[name]

View file

@ -0,0 +1,141 @@
"""Render bootstrap shell scripts for the Aurelio workstation provisioner."""
from __future__ import annotations
import hashlib
import secrets
from datetime import datetime, timezone
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
from .detect import OsFamily, OsInfo, detect_os
from .packages import package_manager_for, packages_for_profile, python_packages_for_profile
from .profiles import VALID_PROFILES, get_profile
_TEMPLATES_DIR = Path(__file__).parent / "templates"
class ProvisionerError(Exception):
"""Raised when provisioning requirements cannot be satisfied."""
_machine_id = lambda name: hashlib.sha256(name.encode()).hexdigest()[:12]
def _now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
def _create_token() -> str:
return secrets.token_urlsafe(32)
def _jinja_env() -> Environment:
return Environment(
loader=FileSystemLoader(_TEMPLATES_DIR),
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)
def render_bootstrap_script(
os_info: OsInfo | None = None,
machine_name: str = "",
profile: str = "embedded",
token: str = "",
public_url: str = "https://savearth-workspace.portugalfuturista.org",
esp_idf_version: str = "v5.5.4",
) -> str:
"""Render a self-contained bootstrap shell script.
Args:
os_info: Detected OS information. If None, detects automatically.
machine_name: Unique machine name. Required.
profile: Toolchain profile (embedded, backend, frontend, hardware, full).
token: One-time registration token. Generated if empty.
public_url: Public savearth-workspace base URL.
esp_idf_version: ESP-IDF branch/tag to install.
Returns:
A shell script as a string.
Raises:
ProvisionerError: If the OS family is unsupported or profile is invalid.
"""
if not machine_name:
raise ProvisionerError("machine_name is required")
if profile not in VALID_PROFILES:
raise ProvisionerError(
f"Unknown profile '{profile}'. Valid: {', '.join(sorted(VALID_PROFILES))}"
)
info = os_info or detect_os()
family = info.family
if family == OsFamily.UNKNOWN:
raise ProvisionerError(
f"Unsupported operating system: {info.distro} {info.version}"
)
prof = get_profile(profile)
pkg_manager = package_manager_for(family)
if pkg_manager is None and family not in (OsFamily.WSL,):
raise ProvisionerError(f"No package manager defined for OS family {family.value}")
base_packages = packages_for_profile(family, profile)
profile_packages: list[str] = []
if family in (OsFamily.DEBIAN, OsFamily.RHEL, OsFamily.ARCH):
# base_packages already includes profile packages from packages_for_profile,
# so split them for nicer script output.
base_only = packages_for_profile(family, "embedded") if profile != "embedded" else base_packages
profile_only = [p for p in base_packages if p not in base_only]
base_packages = base_only
profile_packages = profile_only
elif family == OsFamily.MACOS:
profile_packages = []
context = {
"os_family": family.value,
"distro": info.distro,
"version": info.version,
"is_wsl": info.is_wsl,
"machine_name": machine_name,
"machine_id": _machine_id(machine_name),
"token": token or _create_token(),
"public_url": public_url.rstrip("/"),
"esp_idf_version": esp_idf_version,
"profile": profile,
"generated_at": _now(),
"package_manager": pkg_manager,
"base_packages": base_packages,
"profile_packages": profile_packages,
"python_packages": python_packages_for_profile(profile),
"needs_esp_idf": prof.needs_esp_idf,
"needs_docker": prof.needs_docker,
"needs_node": prof.needs_node,
"needs_github_ssh": prof.needs_github_ssh,
"clones_repos": prof.clones_repos,
"node_install": "", # overridden in per-family templates
}
env = _jinja_env()
template_name = f"{family.value}.sh.j2"
template = env.get_template(template_name)
return template.render(context)
def render_for_current_machine(
machine_name: str = "",
profile: str = "embedded",
**kwargs,
) -> str:
"""Convenience: render a script for the machine this function runs on."""
info = detect_os()
if not machine_name:
import socket
machine_name = socket.gethostname()
return render_bootstrap_script(os_info=info, machine_name=machine_name, profile=profile, **kwargs)

View file

@ -0,0 +1,14 @@
{% extends "base.sh.j2" %}
{% block post_package %}
{% if needs_docker %}
if ! command -v docker &>/dev/null; then
info "Installing Docker..."
sudo pacman -S --noconfirm docker docker-compose || warn "Docker install failed; install manually."
sudo usermod -aG docker "$USER" || true
sudo systemctl enable --now docker || true
fi
{% endif %}
{% endblock %}
{% set node_install = "sudo pacman -S --noconfirm nodejs npm" %}

View file

@ -0,0 +1,202 @@
#!/usr/bin/env bash
# Aurelio workstation bootstrap
# OS family: {{ os_family }}
# Distribution: {{ distro }}
# Profile: {{ profile }}
# Machine: {{ machine_name }}
# Generated: {{ generated_at }}
set -euo pipefail
MACHINE_NAME="{{ machine_name }}"
MACHINE_ID="{{ machine_id }}"
TOKEN="{{ token }}"
PUBLIC_URL="{{ public_url }}"
SAVEARTH_DIR="${SAVEARTH_DIR:-$HOME/savearth}"
ESP_IDF_VERSION="{{ esp_idf_version }}"
PROFILE="{{ profile }}"
log() { echo -e "\033[0;32m[✓]\033[0m $*"; }
warn() { echo -e "\033[1;33m[!]\033[0m $*"; }
err() { echo -e "\033[0;31m[✗]\033[0m $*" >&2; }
info() { echo -e "\033[0;34m[i]\033[0m $*"; }
echo "🔧 Bootstrapping savearth workstation: $MACHINE_NAME"
echo " OS family: {{ os_family }}"
echo " Distribution: {{ distro }}"
echo " Profile: $PROFILE"
{% block pre_package %}{% endblock %}
# ─── 1. System packages ───────────────────────────────────────────────
info "Updating package index..."
{{ package_manager.update_cmd }}
info "Installing base system packages..."
{{ package_manager.install_cmd }} {{ base_packages | join(' ') }}
{% if profile_packages %}
info "Installing profile packages for {{ profile }}..."
{{ package_manager.install_cmd }} {{ profile_packages | join(' ') }}
{% endif %}
{% block post_package %}{% endblock %}
{% if needs_node %}
# ─── 2. Node.js / npx for MCP remote clients ──────────────────────────
if ! command -v npx &>/dev/null; then
info "Installing Node.js..."
{{ node_install }}
fi
{% endif %}
{% if needs_docker %}
# ─── 3. Docker ────────────────────────────────────────────────────────
if ! command -v docker &>/dev/null; then
warn "Docker not found. Please install Docker for your distribution."
warn "See: https://docs.docker.com/engine/install/"
fi
{% endif %}
{% if needs_esp_idf %}
# ─── 4. ESP-IDF ───────────────────────────────────────────────────────
install_esp_idf() {
local idf_dir="$HOME/esp/${ESP_IDF_VERSION}/esp-idf"
if [ -d "$idf_dir" ]; then
log "ESP-IDF already present at $idf_dir"
else
info "Installing ESP-IDF ${ESP_IDF_VERSION}..."
mkdir -p "$HOME/esp/${ESP_IDF_VERSION}"
git clone -b "${ESP_IDF_VERSION}" --recursive \
https://github.com/espressif/esp-idf.git "$idf_dir"
cd "$idf_dir"
./install.sh esp32s3
fi
if ! grep -q "IDF_PATH=\"$idf_dir\"" "$HOME/.bashrc" 2>/dev/null; then
echo "export IDF_PATH=\"$idf_dir\"" >> "$HOME/.bashrc"
echo 'source "$IDF_PATH/export.sh" > /dev/null 2>&1' >> "$HOME/.bashrc"
fi
export IDF_PATH="$idf_dir"
}
install_esp_idf
{% endif %}
# ─── 5. GitHub SSH auth ───────────────────────────────────────────────
setup_github() {
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
info "Generating SSH key for GitHub..."
mkdir -p "$HOME/.ssh"
ssh-keygen -t ed25519 -C "$MACHINE_NAME@savearth" -f "$HOME/.ssh/id_ed25519" -N ""
eval "$(ssh-agent -s)" || true
ssh-add "$HOME/.ssh/id_ed25519" 2>/dev/null || true
fi
if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
log "GitHub SSH public key:"
cat "$HOME/.ssh/id_ed25519.pub"
warn "If this key is not yet added to GitHub, add it now: https://github.com/settings/keys"
if [ -t 0 ]; then
read -rp "Press Enter after verifying the key is added to GitHub..."
fi
fi
}
setup_github
{% if clones_repos %}
# ─── 6. Clone savearth repositories ───────────────────────────────────
clone_or_pull() {
local repo="$1"
local dir="$2"
if [ -d "$dir/.git" ]; then
log "Pulling $repo..."
git -C "$dir" pull
else
log "Cloning $repo..."
git clone "git@github.com:$repo.git" "$dir"
fi
}
mkdir -p "$SAVEARTH_DIR"
cd "$SAVEARTH_DIR"
clone_or_pull SavearthTech/aws-iot-core-poc aws-iot-core-poc
clone_or_pull SavearthTech/savearth-iot-infrastructure savearth-iot-infrastructure
clone_or_pull SavearthTech/savearth-hw-project savearth-hw-project
clone_or_pull SavearthTech/hardware-devicesFirmwareTest hardware-devicesFirmwareTest
# ─── 7. Initialize central-brain submodules ───────────────────────────
for dir in aws-iot-core-poc savearth-iot-infrastructure savearth-hw-project hardware-devicesFirmwareTest; do
log "Initializing replica-omnisciente submodule in $dir..."
git -C "$dir" submodule update --init --recursive replica-omnisciente || warn "Submodule init failed for $dir"
done
{% endif %}
# ─── 8. Python tooling dependencies ───────────────────────────────────
{% if python_packages %}
info "Installing Python tooling dependencies..."
python3 -m pip install --user --upgrade {{ python_packages | join(' ') }} || warn "Some pip packages failed to install"
{% endif %}
# ─── 9. Global Aurelio config ─────────────────────────────────────────
mkdir -p "$HOME/.aurelio"
if [ ! -f "$HOME/.aurelio/config.toml" ]; then
log "Writing ~/.aurelio/config.toml..."
cat > "$HOME/.aurelio/config.toml" << 'TOML'
[sync]
enabled = true
endpoint = "https://mcp.portugalfuturista.org"
interval_seconds = 300
auto_push = true
[identity]
name = "savearth Developer Workstation"
realm = "smart-device-firmware"
[models]
default_local = "qwen2.5-coder:14b"
default_cloud = "gemini-2.5-pro"
ollama_url = "http://127.0.0.1:11434"
[brain]
auto_save = true
artifact_types = ["task", "implementation_plan", "walkthrough", "analysis"]
TOML
else
info "~/.aurelio/config.toml already exists; skipping."
fi
# ─── 10. Register machine and pull brain snapshot ─────────────────────
info "Registering workstation with savearth-workspace..."
curl -sS -X POST "$PUBLIC_URL/api/register" \
-H "Content-Type: application/json" \
-d '{"machine_name":"'$MACHINE_NAME'","os":"{{ os_family }}","token":"'$TOKEN'"}' \
-o /tmp/register.json || true
if [ -s /tmp/register.json ]; then
cat /tmp/register.json
else
warn "Online registration skipped (server may require auth)."
fi
info "Checking for brain snapshots..."
curl -sS "$PUBLIC_URL/api/snapshots/latest?machine_id=$MACHINE_ID" \
-H "Authorization: Bearer $TOKEN" \
-o /tmp/latest_snapshot.json || true
if [ -s /tmp/latest_snapshot.json ]; then
SNAPSHOT_URL=$(python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('download_url',''))" < /tmp/latest_snapshot.json)
if [ -n "$SNAPSHOT_URL" ]; then
log "Restoring brain snapshot..."
curl -sS -H "Authorization: Bearer $TOKEN" "$SNAPSHOT_URL" -o /tmp/brain.tar.gz
for dir in aws-iot-core-poc savearth-iot-infrastructure savearth-hw-project hardware-devicesFirmwareTest; do
[ -d "$dir/replica-omnisciente/.aurelio/brain" ] && \
tar xzf /tmp/brain.tar.gz -C "$dir/replica-omnisciente/.aurelio" 2>/dev/null || true
done
fi
fi
# ─── Done ─────────────────────────────────────────────────────────────
log "Bootstrap complete for machine: $MACHINE_NAME"
echo ""
echo "Next steps:"
echo " 1. Start a new shell or run: source ~/.bashrc"
echo " 2. cd $SAVEARTH_DIR/aws-iot-core-poc"
echo " 3. python3 tools/scripts/build_tool.py env"
echo " 4. Open dashboard: $PUBLIC_URL/dashboard"

View file

@ -0,0 +1,17 @@
{% extends "base.sh.j2" %}
{% block post_package %}
{% if needs_docker %}
if ! command -v docker &>/dev/null; then
info "Installing Docker (best-effort via convenience script)..."
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh || true
if [ -f /tmp/get-docker.sh ]; then
sh /tmp/get-docker.sh || warn "Docker install script failed; install manually."
sudo usermod -aG docker "$USER" || true
sudo systemctl enable --now docker || true
fi
fi
{% endif %}
{% endblock %}
{% set node_install = "sudo apt-get install -y nodejs npm" %}

View file

@ -0,0 +1,20 @@
{% extends "base.sh.j2" %}
{% block pre_package %}
if ! command -v brew &>/dev/null; then
err "Homebrew is required for macOS provisioning."
err "Install from https://brew.sh and re-run this script."
exit 1
fi
{% endblock %}
{% block post_package %}
{% if needs_docker %}
if ! command -v docker &>/dev/null; then
warn "Docker not found. Install Docker Desktop for Mac manually."
warn "See: https://docs.docker.com/desktop/setup/install/mac-install/"
fi
{% endif %}
{% endblock %}
{% set node_install = "brew install node" %}

View file

@ -0,0 +1,14 @@
{% extends "base.sh.j2" %}
{% block post_package %}
{% if needs_docker %}
if ! command -v docker &>/dev/null; then
info "Installing Docker..."
sudo dnf install -y docker docker-compose || warn "Docker install failed; install manually."
sudo usermod -aG docker "$USER" || true
sudo systemctl enable --now docker || true
fi
{% endif %}
{% endblock %}
{% set node_install = "sudo dnf install -y nodejs" %}

View file

@ -0,0 +1,215 @@
#!/usr/bin/env bash
# Aurelio workstation bootstrap for Windows Subsystem for Linux (WSL)
# Underlying distribution: {{ distro }}
# Profile: {{ profile }}
# Machine: {{ machine_name }}
# Generated: {{ generated_at }}
set -euo pipefail
MACHINE_NAME="{{ machine_name }}"
MACHINE_ID="{{ machine_id }}"
TOKEN="{{ token }}"
PUBLIC_URL="{{ public_url }}"
SAVEARTH_DIR="${SAVEARTH_DIR:-$HOME/savearth}"
ESP_IDF_VERSION="{{ esp_idf_version }}"
PROFILE="{{ profile }}"
log() { echo -e "\033[0;32m[✓]\033[0m $*"; }
warn() { echo -e "\033[1;33m[!]\033[0m $*"; }
err() { echo -e "\033[0;31m[✗]\033[0m $*" >&2; }
info() { echo -e "\033[0;34m[i]\033[0m $*"; }
echo "🔧 Bootstrapping savearth WSL workstation: $MACHINE_NAME"
echo " Underlying distribution: {{ distro }}"
echo " Profile: $PROFILE"
echo ""
echo "⚠️ WSL note: USB serial devices (/dev/ttyACM*) are not directly accessible."
echo " Install usbipd-win on Windows and attach devices when flashing:"
echo " https://learn.microsoft.com/en-us/windows/wsl/connect-usb"
echo ""
# Detect package manager from the underlying distro
if command -v apt-get &>/dev/null; then
PKG_UPDATE="sudo apt-get update"
PKG_INSTALL="sudo apt-get install -y --no-install-recommends"
NODE_INSTALL="sudo apt-get install -y nodejs npm"
DOCKER_INSTALL="curl -fsSL https://get.docker.com -o /tmp/get-docker.sh && sh /tmp/get-docker.sh"
elif command -v dnf &>/dev/null; then
PKG_UPDATE="sudo dnf update -y"
PKG_INSTALL="sudo dnf install -y"
NODE_INSTALL="sudo dnf install -y nodejs"
DOCKER_INSTALL="sudo dnf install -y docker docker-compose"
else
err "Unsupported WSL distribution: {{ distro }}"
exit 1
fi
# ─── 1. System packages ───────────────────────────────────────────────
info "Updating package index..."
$PKG_UPDATE
info "Installing base system packages..."
$PKG_INSTALL {{ base_packages | join(' ') }}
{% if profile_packages %}
info "Installing profile packages for {{ profile }}..."
$PKG_INSTALL {{ profile_packages | join(' ') }}
{% endif %}
{% if needs_node %}
# ─── 2. Node.js / npx ─────────────────────────────────────────────────
if ! command -v npx &>/dev/null; then
info "Installing Node.js..."
$NODE_INSTALL
fi
{% endif %}
{% if needs_docker %}
# ─── 3. Docker ────────────────────────────────────────────────────────
if ! command -v docker &>/dev/null; then
info "Installing Docker..."
$DOCKER_INSTALL || warn "Docker install failed; install manually."
fi
{% endif %}
{% if needs_esp_idf %}
# ─── 4. ESP-IDF ───────────────────────────────────────────────────────
install_esp_idf() {
local idf_dir="$HOME/esp/${ESP_IDF_VERSION}/esp-idf"
if [ -d "$idf_dir" ]; then
log "ESP-IDF already present at $idf_dir"
else
info "Installing ESP-IDF ${ESP_IDF_VERSION}..."
mkdir -p "$HOME/esp/${ESP_IDF_VERSION}"
git clone -b "${ESP_IDF_VERSION}" --recursive \
https://github.com/espressif/esp-idf.git "$idf_dir"
cd "$idf_dir"
./install.sh esp32s3
fi
if ! grep -q "IDF_PATH=\"$idf_dir\"" "$HOME/.bashrc" 2>/dev/null; then
echo "export IDF_PATH=\"$idf_dir\"" >> "$HOME/.bashrc"
echo 'source "$IDF_PATH/export.sh" > /dev/null 2>&1' >> "$HOME/.bashrc"
fi
export IDF_PATH="$idf_dir"
}
install_esp_idf
{% endif %}
# ─── 5. GitHub SSH auth ───────────────────────────────────────────────
setup_github() {
if [ ! -f "$HOME/.ssh/id_ed25519" ]; then
info "Generating SSH key for GitHub..."
mkdir -p "$HOME/.ssh"
ssh-keygen -t ed25519 -C "$MACHINE_NAME@savearth" -f "$HOME/.ssh/id_ed25519" -N ""
eval "$(ssh-agent -s)" || true
ssh-add "$HOME/.ssh/id_ed25519" 2>/dev/null || true
fi
if [ -f "$HOME/.ssh/id_ed25519.pub" ]; then
log "GitHub SSH public key:"
cat "$HOME/.ssh/id_ed25519.pub"
warn "If this key is not yet added to GitHub, add it now: https://github.com/settings/keys"
if [ -t 0 ]; then
read -rp "Press Enter after verifying the key is added to GitHub..."
fi
fi
}
setup_github
{% if clones_repos %}
# ─── 6. Clone savearth repositories ───────────────────────────────────
clone_or_pull() {
local repo="$1"
local dir="$2"
if [ -d "$dir/.git" ]; then
log "Pulling $repo..."
git -C "$dir" pull
else
log "Cloning $repo..."
git clone "git@github.com:$repo.git" "$dir"
fi
}
mkdir -p "$SAVEARTH_DIR"
cd "$SAVEARTH_DIR"
clone_or_pull SavearthTech/aws-iot-core-poc aws-iot-core-poc
clone_or_pull SavearthTech/savearth-iot-infrastructure savearth-iot-infrastructure
clone_or_pull SavearthTech/savearth-hw-project savearth-hw-project
clone_or_pull SavearthTech/hardware-devicesFirmwareTest hardware-devicesFirmwareTest
# ─── 7. Initialize central-brain submodules ───────────────────────────
for dir in aws-iot-core-poc savearth-iot-infrastructure savearth-hw-project hardware-devicesFirmwareTest; do
log "Initializing replica-omnisciente submodule in $dir..."
git -C "$dir" submodule update --init --recursive replica-omnisciente || warn "Submodule init failed for $dir"
done
{% endif %}
# ─── 8. Python tooling dependencies ───────────────────────────────────
{% if python_packages %}
info "Installing Python tooling dependencies..."
python3 -m pip install --user --upgrade {{ python_packages | join(' ') }} || warn "Some pip packages failed to install"
{% endif %}
# ─── 9. Global Aurelio config ─────────────────────────────────────────
mkdir -p "$HOME/.aurelio"
if [ ! -f "$HOME/.aurelio/config.toml" ]; then
log "Writing ~/.aurelio/config.toml..."
cat > "$HOME/.aurelio/config.toml" << 'TOML'
[sync]
enabled = true
endpoint = "https://mcp.portugalfuturista.org"
interval_seconds = 300
auto_push = true
[identity]
name = "savearth Developer Workstation"
realm = "smart-device-firmware"
[models]
default_local = "qwen2.5-coder:14b"
default_cloud = "gemini-2.5-pro"
ollama_url = "http://127.0.0.1:11434"
[brain]
auto_save = true
artifact_types = ["task", "implementation_plan", "walkthrough", "analysis"]
TOML
else
info "~/.aurelio/config.toml already exists; skipping."
fi
# ─── 10. Register machine and pull brain snapshot ─────────────────────
info "Registering workstation with savearth-workspace..."
curl -sS -X POST "$PUBLIC_URL/api/register" \
-H "Content-Type: application/json" \
-d '{"machine_name":"'$MACHINE_NAME'","os":"wsl","token":"'$TOKEN'"}' \
-o /tmp/register.json || true
if [ -s /tmp/register.json ]; then
cat /tmp/register.json
else
warn "Online registration skipped (server may require auth)."
fi
info "Checking for brain snapshots..."
curl -sS "$PUBLIC_URL/api/snapshots/latest?machine_id=$MACHINE_ID" \
-H "Authorization: Bearer $TOKEN" \
-o /tmp/latest_snapshot.json || true
if [ -s /tmp/latest_snapshot.json ]; then
SNAPSHOT_URL=$(python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('download_url',''))" < /tmp/latest_snapshot.json)
if [ -n "$SNAPSHOT_URL" ]; then
log "Restoring brain snapshot..."
curl -sS -H "Authorization: Bearer $TOKEN" "$SNAPSHOT_URL" -o /tmp/brain.tar.gz
for dir in aws-iot-core-poc savearth-iot-infrastructure savearth-hw-project hardware-devicesFirmwareTest; do
[ -d "$dir/replica-omnisciente/.aurelio/brain" ] && \
tar xzf /tmp/brain.tar.gz -C "$dir/replica-omnisciente/.aurelio" 2>/dev/null || true
done
fi
fi
# ─── Done ─────────────────────────────────────────────────────────────
log "Bootstrap complete for machine: $MACHINE_NAME"
echo ""
echo "Next steps:"
echo " 1. Start a new shell or run: source ~/.bashrc"
echo " 2. cd $SAVEARTH_DIR/aws-iot-core-poc"
echo " 3. python3 tools/scripts/build_tool.py env"
echo " 4. Open dashboard: $PUBLIC_URL/dashboard"

View file

View file

@ -0,0 +1,29 @@
"""Tests for OS family detection."""
import pytest
from provisioner.detect import OsFamily, OsInfo, _map_linux_distro, _read_os_release
@pytest.mark.parametrize(
"os_id, id_like, expected",
[
("ubuntu", "debian", OsFamily.DEBIAN),
("debian", "", OsFamily.DEBIAN),
("linuxmint", "debian ubuntu", OsFamily.DEBIAN),
("fedora", "", OsFamily.RHEL),
("rocky", "rhel centos fedora", OsFamily.RHEL),
("rhel", "", OsFamily.RHEL),
("arch", "", OsFamily.ARCH),
("manjaro", "arch", OsFamily.ARCH),
("gentoo", "", OsFamily.UNKNOWN),
],
)
def test_map_linux_distro(os_id: str, id_like: str, expected: OsFamily):
release = {"ID": os_id, "ID_LIKE": id_like}
assert _map_linux_distro(release) == expected
def test_os_info_is_linux():
assert OsInfo(OsFamily.DEBIAN, "ubuntu", "24.04", False).is_linux
assert not OsInfo(OsFamily.MACOS, "macos", "14", False).is_linux

View file

@ -0,0 +1,56 @@
"""Tests for bootstrap script rendering."""
import subprocess
import pytest
from provisioner import ProvisionerError, render_bootstrap_script
from provisioner.detect import OsFamily, OsInfo
@pytest.mark.parametrize(
"family, profile",
[
(OsFamily.DEBIAN, "embedded"),
(OsFamily.RHEL, "embedded"),
(OsFamily.ARCH, "embedded"),
(OsFamily.MACOS, "embedded"),
(OsFamily.WSL, "embedded"),
(OsFamily.DEBIAN, "full"),
(OsFamily.RHEL, "backend"),
(OsFamily.RHEL, "hardware"),
],
)
def test_rendered_script_is_valid_bash(family: OsFamily, profile: str):
os_info = OsInfo(family, family.value, "test", family == OsFamily.WSL)
script = render_bootstrap_script(
os_info=os_info,
machine_name="test-laptop",
profile=profile,
token="test-token",
)
assert script.startswith("#!/usr/bin/env bash")
result = subprocess.run(["bash", "-n"], input=script, text=True, capture_output=True)
assert result.returncode == 0, result.stderr
def test_invalid_profile_raises():
os_info = OsInfo(OsFamily.DEBIAN, "debian", "", False)
with pytest.raises(ProvisionerError):
render_bootstrap_script(os_info=os_info, machine_name="x", profile="not-a-profile")
def test_empty_machine_name_raises():
os_info = OsInfo(OsFamily.DEBIAN, "debian", "", False)
with pytest.raises(ProvisionerError):
render_bootstrap_script(os_info=os_info, machine_name="", profile="embedded")
def test_family_package_lists_are_present():
from provisioner.packages import BASE_PACKAGES, PROFILE_PACKAGES
for family in (OsFamily.DEBIAN, OsFamily.RHEL, OsFamily.ARCH, OsFamily.MACOS):
assert BASE_PACKAGES.get(family), f"Missing base packages for {family}"
for profile in ("embedded", "backend", "frontend", "hardware"):
assert profile in PROFILE_PACKAGES, f"Missing profile {profile}"
assert family in PROFILE_PACKAGES[profile], f"Missing {family} in {profile}"