# Pi Quickstart Script Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Ship `scripts/quickstart-pi.sh`, a self-contained `curl | sudo bash` installer that turns a fresh 64-bit Raspberry Pi OS Lite host into a running, mDNS-discoverable `sendspin-player` daemon using the prebuilt arm64 release tarball. **Architecture:** A single Bash script under `set -euo pipefail` with phase-named functions, fetched verbatim from `raw.githubusercontent.com`. Reuses every artifact already in the repo: the arm64 release tarball, `dist/systemd/sendspin-player.service`, `dist/systemd/sendspin-player.env`, and `dist/config/player.example.yaml` — all pulled at the resolved release tag's ref. No code changes inside `pkg/` or `cmd/`. Per-task verification is `shellcheck`; final acceptance is a smoke run inside an `arm64v8/debian:bookworm` Docker container plus a manual Pi install. **Tech Stack:** Bash 5.x, `curl`, `tar`, `apt-get`, `systemctl`, `shellcheck` (per-task verify), Docker with QEMU for arm64 emulation (final smoke test). No new runtime dependencies on the user's Pi beyond what `apt-get` installs. **Spec:** [docs/superpowers/specs/2026-05-01-quickstart-pi-design.md](../specs/2026-05-01-quickstart-pi-design.md) --- ## File Structure - **Create:** `scripts/quickstart-pi.sh` — the entire installer (single file, all phases as functions, `main()` at the bottom) - **Modify:** `.github/workflows/ci.yml` — add a shellcheck step to the existing `lint` job - **Modify:** `README.md` — add a "Quickstart on Raspberry Pi" section near the existing install instructions The script lives in `scripts/` (a new directory) rather than the repo root because the root already hosts `install-deps.sh` (build-time deps, source-build users) — keeping the prebuilt-binary installer in `scripts/` makes the distinction explicit. Per-task commits use the `feat(quickstart):` scope on functional additions, `chore(ci):` for the lint step, and `docs(readme):` for the README change. **Prerequisite for the implementer:** install `shellcheck` locally (`brew install shellcheck` / `apt-get install shellcheck`). Every task ends with a `shellcheck` invocation. --- ## Task 1: Script skeleton with stubbed phases and `--help` **Files:** - Create: `scripts/quickstart-pi.sh` - [ ] **Step 1: Create the directory and write the skeleton** ```bash mkdir -p scripts ``` Write `scripts/quickstart-pi.sh`: ```bash #!/usr/bin/env bash # ABOUTME: One-shot installer for sendspin-player on 64-bit Raspberry Pi OS. # ABOUTME: Fetches the latest arm64 release tarball, installs systemd unit, starts daemon. set -euo pipefail readonly REPO_OWNER="Sendspin" readonly REPO_NAME="sendspin-go" readonly REPO_URL="https://github.com/${REPO_OWNER}/${REPO_NAME}" readonly RAW_URL_BASE="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}" readonly BINARY_NAME="sendspin-player" readonly INSTALL_PATH="/usr/local/bin/${BINARY_NAME}" readonly UNIT_PATH="/etc/systemd/system/${BINARY_NAME}.service" readonly ENV_PATH="/etc/default/${BINARY_NAME}" readonly CONFIG_DIR="/etc/sendspin" readonly CONFIG_PATH="${CONFIG_DIR}/player.yaml" # Set by parse_args ARG_NAME="" ARG_DEVICE="" ARG_VERSION="" ARG_UNINSTALL=0 # Resolved by resolve_version RESOLVED_TAG="" RESOLVED_REF="" usage() { cat <] [--device ] [--version ] [--uninstall] Installs sendspin-player as a systemd daemon on 64-bit Raspberry Pi OS. Options: --name Friendly player name (default: -sendspin-player). --device Exact audio device name. Run 'sendspin-player --list-audio-devices' after install to discover available names. --version Pin to a specific release tag (e.g. v1.6.2). Default: latest. --uninstall Stop the service and remove the binary and unit file. Config is preserved. -h, --help Show this help. Run with sudo: curl -fsSL ${RAW_URL_BASE}/main/scripts/quickstart-pi.sh | sudo bash curl -fsSL ${RAW_URL_BASE}/main/scripts/quickstart-pi.sh | sudo bash -s -- --name "Living Room" EOF } log() { printf '==> %s\n' "$*"; } warn() { printf 'WARN: %s\n' "$*" >&2; } die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } parse_args() { :; } preflight() { :; } do_uninstall() { :; } install_apt_deps() { :; } resolve_version() { :; } stop_service() { :; } install_binary() { :; } install_unit() { :; } install_env() { :; } install_config() { :; } start_and_verify() { :; } main() { parse_args "$@" preflight if [[ "${ARG_UNINSTALL}" -eq 1 ]]; then do_uninstall exit 0 fi install_apt_deps resolve_version stop_service install_binary install_unit install_env install_config start_and_verify } main "$@" ``` - [ ] **Step 2: Make executable and run shellcheck** ```bash chmod +x scripts/quickstart-pi.sh shellcheck scripts/quickstart-pi.sh ``` Expected: clean exit 0, no warnings. - [ ] **Step 3: Verify `--help` works in isolation** ```bash bash scripts/quickstart-pi.sh --help 2>&1 | head -5 ``` Note: this will currently fail because `parse_args` is a no-op stub — it falls through to `preflight` which is also a no-op, then continues to `install_apt_deps` etc. That's expected; we're just verifying the syntax parses. Use `bash -n scripts/quickstart-pi.sh` to confirm syntax-only: ```bash bash -n scripts/quickstart-pi.sh ``` Expected: clean exit 0. - [ ] **Step 4: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): script skeleton with stubbed phases" ``` --- ## Task 2: Argument parser and `--help` handling **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `parse_args` stub) - [ ] **Step 1: Replace the `parse_args` stub** Find: ```bash parse_args() { :; } ``` Replace with: ```bash parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --name) [[ $# -ge 2 ]] || die "--name requires a value" ARG_NAME="$2" shift 2 ;; --device) [[ $# -ge 2 ]] || die "--device requires a value" ARG_DEVICE="$2" shift 2 ;; --version) [[ $# -ge 2 ]] || die "--version requires a value (e.g. v1.6.2)" ARG_VERSION="$2" shift 2 ;; --uninstall) ARG_UNINSTALL=1 shift ;; -h|--help) usage exit 0 ;; *) printf 'Unknown argument: %s\n\n' "$1" >&2 usage >&2 exit 2 ;; esac done } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Smoke test the parser by hand** ```bash bash scripts/quickstart-pi.sh --help # prints usage, exits 0 bash scripts/quickstart-pi.sh --bogus 2>&1 | head -3 # prints "Unknown argument" + usage, exits 2 echo "exit=$?" ``` Expected: `--help` exits 0; `--bogus` exits 2. (Don't run it with valid args yet — preflight is still a stub and would let the script fall through to apt-get without arch-checking. That's the next task.) - [ ] **Step 4: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): argument parser with --name/--device/--version/--uninstall" ``` --- ## Task 3: Pre-flight checks (root, arch, debian, systemd) **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `preflight` stub) - [ ] **Step 1: Replace the `preflight` stub** Find: ```bash preflight() { :; } ``` Replace with: ```bash preflight() { if [[ "${EUID}" -ne 0 ]]; then die "Root required. Re-run with sudo: curl -fsSL ${RAW_URL_BASE}/main/scripts/quickstart-pi.sh | sudo bash" fi local arch arch="$(uname -m)" if [[ "${arch}" != "aarch64" ]]; then die "Unsupported architecture: ${arch}. This script supports 64-bit Raspberry Pi OS only (aarch64). For Pi 3 / 4 / 5 / Zero 2 W, install the 64-bit Pi OS image: https://www.raspberrypi.com/software/operating-systems/ Pi 1 / Zero (v1) / Zero W are not supported (32-bit ARMv6 only)." fi if [[ ! -f /etc/debian_version ]]; then die "Unsupported OS. This script targets Debian-based distros (Pi OS, Raspberry Pi OS Lite). For other distros see the README install steps: ${REPO_URL}#installation" fi if ! command -v systemctl >/dev/null 2>&1; then die "systemctl not found. The quickstart installs sendspin-player as a systemd service; non-systemd hosts must follow the manual install steps." fi } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Verify behavior on the dev machine** ```bash bash scripts/quickstart-pi.sh --help # prints usage, exits 0 (preflight not yet reached) bash scripts/quickstart-pi.sh 2>&1 | head -3 ``` Expected: the second command fails with "Root required" if you're not root, or with "Unsupported architecture" if you're root on a non-aarch64 dev machine. Either of those is correct — both prove preflight runs. - [ ] **Step 4: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): preflight checks for root, aarch64, debian, systemd" ``` --- ## Task 4: Uninstall short-circuit **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `do_uninstall` stub) - [ ] **Step 1: Replace the `do_uninstall` stub** Find: ```bash do_uninstall() { :; } ``` Replace with: ```bash do_uninstall() { log "Uninstalling sendspin-player..." if systemctl list-unit-files "${BINARY_NAME}.service" >/dev/null 2>&1; then systemctl disable --now "${BINARY_NAME}.service" 2>/dev/null || true fi rm -f "${INSTALL_PATH}" rm -f "${UNIT_PATH}" systemctl daemon-reload log "Uninstall complete." log "Config preserved at ${CONFIG_DIR}/ and ${ENV_PATH}." log "Remove manually for a full purge:" log " sudo rm -rf ${CONFIG_DIR} ${ENV_PATH}" } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): --uninstall removes binary and unit, preserves config" ``` --- ## Task 5: Apt runtime deps **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `install_apt_deps` stub) - [ ] **Step 1: Replace the `install_apt_deps` stub** Find: ```bash install_apt_deps() { :; } ``` Replace with: ```bash install_apt_deps() { log "Installing runtime dependencies..." DEBIAN_FRONTEND=noninteractive apt-get update -qq DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ libopus0 \ libopusfile0 \ libflac12 \ libasound2 \ ca-certificates \ curl \ tar } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): install runtime deps via apt-get" ``` --- ## Task 6: Version resolution **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `resolve_version` stub) - [ ] **Step 1: Replace the `resolve_version` stub** Find: ```bash resolve_version() { :; } ``` Replace with: ```bash resolve_version() { if [[ -n "${ARG_VERSION}" ]]; then RESOLVED_TAG="${ARG_VERSION}" RESOLVED_REF="${ARG_VERSION}" log "Installing pinned version: ${RESOLVED_TAG}" else # Use GitHub's latest-release redirect: no API call, no JSON parsing. # The redirect target tells us the resolved tag. local redirect_url redirect_url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ "${REPO_URL}/releases/latest")" \ || die "Failed to resolve latest release tag from ${REPO_URL}/releases/latest" RESOLVED_TAG="${redirect_url##*/}" # The dist/ files at "main" are forward-compatible enough for the # latest-tagged release; pin them to the same tag for consistency. RESOLVED_REF="${RESOLVED_TAG}" log "Installing latest release: ${RESOLVED_TAG}" fi } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): resolve version via GitHub latest-release redirect" ``` --- ## Task 7: Stop service if running **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `stop_service` stub) - [ ] **Step 1: Replace the `stop_service` stub** Find: ```bash stop_service() { :; } ``` Replace with: ```bash stop_service() { if systemctl is-active --quiet "${BINARY_NAME}.service"; then log "Stopping running ${BINARY_NAME} service..." systemctl stop "${BINARY_NAME}.service" fi } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): stop running service before binary swap" ``` --- ## Task 8: Download, extract, and install the binary **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `install_binary` stub) - [ ] **Step 1: Replace the `install_binary` stub** Find: ```bash install_binary() { :; } ``` Replace with: ```bash install_binary() { local tarball_url tarball_name tmpdir tarball_name="${BINARY_NAME}-linux-arm64.tar.gz" tarball_url="${REPO_URL}/releases/download/${RESOLVED_TAG}/${tarball_name}" tmpdir="$(mktemp -d)" # shellcheck disable=SC2064 trap "rm -rf '${tmpdir}'" EXIT log "Downloading ${tarball_url}..." curl -fSL "${tarball_url}" -o "${tmpdir}/${tarball_name}" \ || die "Failed to download release tarball from ${tarball_url}" log "Extracting..." tar -xzf "${tmpdir}/${tarball_name}" -C "${tmpdir}" if [[ ! -f "${tmpdir}/${BINARY_NAME}" ]]; then die "Tarball did not contain expected binary '${BINARY_NAME}'" fi log "Installing ${INSTALL_PATH}..." install -m 755 "${tmpdir}/${BINARY_NAME}" "${INSTALL_PATH}" } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean (the `SC2064` disable is for the trap quoting, which is intentional — we want `${tmpdir}` expanded at trap-set time, not trap-fire time). - [ ] **Step 3: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): download release tarball into tempdir, install binary" ``` --- ## Task 9: Install the systemd unit **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `install_unit` stub) - [ ] **Step 1: Replace the `install_unit` stub** Find: ```bash install_unit() { :; } ``` Replace with: ```bash install_unit() { local unit_url unit_url="${RAW_URL_BASE}/${RESOLVED_REF}/dist/systemd/${BINARY_NAME}.service" log "Installing systemd unit ${UNIT_PATH}..." curl -fSL "${unit_url}" -o "${UNIT_PATH}" \ || die "Failed to download unit file from ${unit_url}" chmod 644 "${UNIT_PATH}" } ``` - [ ] **Step 2: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 3: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): install systemd unit from tagged ref" ``` --- ## Task 10: Install the env file (with flag handling) **Files:** - Modify: `scripts/quickstart-pi.sh` (replace `install_env` stub) - [ ] **Step 1: Replace the `install_env` stub** Find: ```bash install_env() { :; } ``` Replace with: ```bash # shell_quote: wrap a string in single quotes, escaping any embedded single # quotes via the standard '\'' pattern. Safe for arbitrary user input. shell_quote() { local s="$1" s="${s//\'/\'\\\'\'}" printf "'%s'" "${s}" } install_env() { if [[ -n "${ARG_NAME}" || -n "${ARG_DEVICE}" ]]; then log "Writing ${ENV_PATH} with --name/--device from flags..." local opts="" if [[ -n "${ARG_NAME}" ]]; then opts+="--name $(shell_quote "${ARG_NAME}") " fi if [[ -n "${ARG_DEVICE}" ]]; then opts+="--audio-device $(shell_quote "${ARG_DEVICE}") " fi # Trim trailing space. opts="${opts% }" cat >"${ENV_PATH}" <&2 fi } trap on_exit EXIT ``` (Note: this `EXIT` trap coexists with the `install_binary` tempdir-cleanup trap. The tempdir trap is set inside that function and replaces this one only for the duration of `install_binary`. To avoid the conflict, change `install_binary`'s trap to be additive — see step 3.) - [ ] **Step 3: Make `install_binary`'s trap additive** In `install_binary`, replace: ```bash # shellcheck disable=SC2064 trap "rm -rf '${tmpdir}'" EXIT ``` With: ```bash # shellcheck disable=SC2064 trap "rm -rf '${tmpdir}'; on_exit" EXIT ``` This preserves both behaviors: tempdir cleanup AND the failure hint. - [ ] **Step 4: Run shellcheck** ```bash shellcheck scripts/quickstart-pi.sh ``` Expected: clean. - [ ] **Step 5: Run a syntax check** ```bash bash -n scripts/quickstart-pi.sh ``` Expected: clean exit 0. - [ ] **Step 6: Commit** ```bash git add scripts/quickstart-pi.sh git commit -m "feat(quickstart): start/enable service, health-check, success message" ``` --- ## Task 13: Add shellcheck to CI **Files:** - Modify: `.github/workflows/ci.yml` - [ ] **Step 1: Add shellcheck install + step to the lint job** Open `.github/workflows/ci.yml`. Find the `lint` job's "Install dependencies" step: ```yaml - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y libopus-dev libopusfile-dev libasound2-dev ``` Replace with: ```yaml - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y libopus-dev libopusfile-dev libasound2-dev shellcheck ``` Then, immediately before the `- name: golangci-lint` step in the same job, add: ```yaml - name: Shellcheck run: shellcheck scripts/*.sh install-deps.sh ``` (`install-deps.sh` is included so we get coverage on the existing script too — since shellcheck is now available in CI, there's no reason to leave it unchecked.) - [ ] **Step 2: Validate the YAML locally** ```bash python -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))" \ && echo "yaml ok" ``` Expected: `yaml ok`. (If `python` isn't available, use `yq . .github/workflows/ci.yml` or `cat .github/workflows/ci.yml` and eyeball indentation.) - [ ] **Step 3: Run shellcheck locally on both scripts to confirm both pass** ```bash shellcheck scripts/quickstart-pi.sh install-deps.sh ``` Expected: clean exit 0. (If `install-deps.sh` produces warnings that aren't introduced by this PR, fix them in a separate commit before merging this task — don't bundle pre-existing fixes into the quickstart change.) - [ ] **Step 4: Commit** ```bash git add .github/workflows/ci.yml git commit -m "chore(ci): run shellcheck on shell scripts in lint job" ``` --- ## Task 14: README quickstart section **Files:** - Modify: `README.md` - [ ] **Step 1: Add a "Quickstart on Raspberry Pi" section** Open `README.md`. Find the existing install instructions (search for the "Install the library" or build-from-source section near the top). Immediately *before* the build-from-source instructions, add: ```markdown ## Quickstart on Raspberry Pi For a 64-bit Raspberry Pi OS (Lite is recommended; Bookworm or newer required): ```bash curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-go/main/scripts/quickstart-pi.sh | sudo bash ``` The script installs runtime dependencies, downloads the latest `sendspin-player-linux-arm64` release tarball, and registers the player as a systemd service. Add flags after `--` to pre-configure the player without editing files afterwards: ```bash curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-go/main/scripts/quickstart-pi.sh \ | sudo bash -s -- --name "Living Room" --device "USB Audio Device" ``` Pin to a specific release with `--version v1.6.2`. Remove the player with `--uninstall` (config in `/etc/sendspin/` is preserved). Supported on Pi 3 / 4 / 5 / Zero 2 W; not supported on 32-bit-only hardware (Pi 1 / Zero v1 / Zero W). After install: - View live logs: `journalctl -u sendspin-player -f` - Discover device names: `sendspin-player --list-audio-devices` - Edit config: `sudo nano /etc/sendspin/player.yaml` ``` (Adjust the surrounding heading levels if the README uses `###` for that depth — match what's already there.) - [ ] **Step 2: Eyeball the rendered output** ```bash git diff README.md | head -40 ``` Confirm the markdown reads cleanly and the surrounding headings still flow. - [ ] **Step 3: Commit** ```bash git add README.md git commit -m "docs(readme): add Pi quickstart section" ``` --- ## Task 15: Smoke test in arm64 Docker **Files:** none (verification-only) This task verifies the script works end-to-end without needing a physical Pi. It uses Docker's QEMU-based arm64 emulation to simulate the target environment. The smoke test is intentionally not a CI check — it's a developer-machine acceptance step before merge. Systemd flows can't be exercised in a default Docker container, so we cover everything *up to* the systemd-reload step and stop there. - [ ] **Step 1: Ensure Docker buildx + qemu-static are available** ```bash docker run --rm --privileged multiarch/qemu-user-static --reset -p yes ``` (One-time setup on the dev machine. Idempotent.) - [ ] **Step 2: Run an interactive arm64 Bookworm container** ```bash docker run --rm -it --platform linux/arm64 \ -v "$(pwd)/scripts/quickstart-pi.sh:/tmp/quickstart-pi.sh:ro" \ arm64v8/debian:bookworm bash ``` - [ ] **Step 3: Inside the container, install systemctl shim and run the script** In the container: ```bash apt-get update -qq && apt-get install -y -qq curl ca-certificates # Provide a no-op systemctl so the script's preflight passes; we'll skip # the actual systemd interaction in the next step. cat >/usr/local/bin/systemctl <<'EOF' #!/bin/bash echo "systemctl-shim: $*" >&2 case "$1" in is-active) exit 1 ;; # pretend service is not active list-unit-files) exit 0 ;; *) exit 0 ;; esac EOF chmod +x /usr/local/bin/systemctl bash /tmp/quickstart-pi.sh --version v1.6.2 --name "Smoke Test" ``` Expected: the script downloads the v1.6.2 tarball, installs the binary at `/usr/local/bin/sendspin-player`, writes the unit file, writes `/etc/default/sendspin-player` with `SENDSPIN_PLAYER_OPTS="--name 'Smoke Test'"`, and writes `/etc/sendspin/player.yaml`. Final "service failed to come up" warning is *expected* — the systemctl shim returns non-zero on `is-active`, which surfaces the health-check failure path. That confirms the health check works. - [ ] **Step 4: Inside the container, sanity-check the installed artifacts** ```bash ls -la /usr/local/bin/sendspin-player /etc/systemd/system/sendspin-player.service \ /etc/default/sendspin-player /etc/sendspin/player.yaml cat /etc/default/sendspin-player /usr/local/bin/sendspin-player --version ``` Expected: all four files present; env file shows `--name 'Smoke Test'`; binary prints v1.6.2. - [ ] **Step 5: Run idempotency check** In the same container: ```bash bash /tmp/quickstart-pi.sh --version v1.6.2 ``` Expected: clean run, "Preserving existing /etc/sendspin/player.yaml" message, env file *not* rewritten (still shows the prior `Smoke Test` name). - [ ] **Step 6: Run uninstall** ```bash bash /tmp/quickstart-pi.sh --uninstall ls /usr/local/bin/sendspin-player /etc/systemd/system/sendspin-player.service 2>&1 ls /etc/sendspin/player.yaml /etc/default/sendspin-player ``` Expected: binary and unit gone; config dir and env file preserved. - [ ] **Step 7: Final pass — real Pi** On a fresh 64-bit Pi OS Lite install: ```bash curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-go/main/scripts/quickstart-pi.sh | sudo bash ``` Verify: the player appears in mDNS / Music Assistant within ~10 seconds, audio plays cleanly, `journalctl -u sendspin-player -f` shows the expected "connected to server" / chunk-decode logs. - [ ] **Step 8: No commit needed for this task** — it's verification-only. If anything failed in steps 3–7, file fixes as additional commits and re-run from step 3.