# Development Guide How this repo works, how to add features, and what to keep in mind when editing. --- ## Architecture ### Installation Phases ``` install.sh │ ┌───────────┼───────────┐ ▼ ▼ ▼ preinstall.sh bin/* postinstall.sh (packages) → /usr/local/bin (config + services) ``` | Phase | Script | Responsibility | |-------|--------|----------------| | Pre | `preinstall.sh` | System packages, apt repos, global binaries (yt-dlp) | | Install | `install.sh` | Copies `bin/*` → `/usr/local/bin/` (chmod 755), `lib/common.sh` → `/usr/local/bin/common.sh` | | Post | `postinstall.sh` | User config (SSH keys, PATH, bash completion), systemd services | Each phase is independent and runs only if the corresponding script exists. ### Directory Layout | Directory | Purpose | Installed To | |-----------|---------|-------------| | `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` | | `apps//` | Optional desktop app installers | run on demand | | `lib/` | Shared library (`common.sh`) | sourced at build time | | `config/` | Gitignored user config files | `~/.config//` (via postinstall) | | `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` | | `systemd/` | Systemd unit files | `/etc/systemd/system/` (via postinstall) | ### The `pos` CLI `bin/pos` is a smart dispatcher. It scans its own directory for executable `pos-*` files and uses variable-length argument matching: ``` pos docker compose up jellyfin → tries pos-docker-compose-up-jellyfin (not found) → tries pos-docker-compose-up (not found) → finds pos-docker-compose → runs with args "up jellyfin" ``` All non-interactive commands log to `~/.local/share/linux_post_install/logs/`. ### Shared Library (`lib/common.sh`) Sourced by most scripts. Key functions: | Function | Purpose | |----------|---------| | `log "msg"` | Green `[+]` status message | | `warn "msg"` | Yellow `[!]` warning | | `err "msg"` | Red `ERROR:` + exit 1 | | `ok "msg"` | Green `OK` prefix | | `section "title"` | Cyan-bordered section header | | `step N T "msg"` | Numbered step header | | `run cmd` | Executes command, respects `$DRY_RUN` | | `spawn "msg" cmd` | Animated braille spinner + elapsed time | | `timer_start` / `timer_stop` | Elapsed time tracking | | `confirm "prompt"` | y/N prompt with optional default | --- ## Adding a New CLI Tool ### 1. Create the script ```bash #!/usr/bin/env bash set -euo pipefail source "$(dirname "$0")/../lib/common.sh" usage() { cat < EOF exit 0 } case "${1:-}" in -h|--help|"") usage ;; esac # --- script logic --- ``` **Conventions:** - Shebang: `#!/usr/bin/env bash` - Strict mode: `set -euo pipefail` - `--help` flag: accept `-h` / `--help` via `case` pattern - Shared library: always source `common.sh` for colors, logging, spinners - Exit codes: `0` success, `1` error - No shared lib? Inline fallbacks: ```bash log() { echo "[+] $*"; } warn() { echo "[!] $*"; } err() { echo "ERROR: $*" >&2; exit 1; } ``` ### 2. Add system dependencies Add package names to the `PACKAGES` array in `preinstall.sh`: ```bash PACKAGES=( ... your-package ) ``` ### 3. Add config files (if needed) Place defaults in `config/` and add copy logic to `postinstall.sh`. If they contain secrets, add to `.gitignore` and document in README. ### 4. Add SSH keys (if needed) Place public keys in `config/authorized_keys` (one per line). `postinstall.sh` reads this file automatically. ### 5. Update README Add a section under the relevant category in README.md. ### 6. Test ```bash bash -n bin/your-tool shellcheck bin/your-tool ./bin/your-tool --help ``` --- ## Adding an Optional App ### 1. Create the installer ```bash #!/usr/bin/env bash set -euo pipefail source "$(dirname "$0")/../../lib/common.sh" install_myapp() { command -v myapp &>/dev/null && { log "myapp already installed"; return 0; } spawn "Installing myapp" sudo apt install -y myapp } install_myapp ``` Place it in `apps//.sh`. It auto-appears in the picker — no registration needed. **Categories:** `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities` ### 2. Conventions - Idempotent: check `command -v` before installing - APT packages → `sudo apt install -y` inside `spawn` - Official scripts → `curl ... | sh` inside `spawn` - Flatpak → `flatpak install -y flathub ` inside `spawn` - `.deb` files → download to temp, `sudo apt install -y ./file.deb` inside `spawn` - `usermod` for groups → print re-login reminder --- ## Editing an Existing Tool 1. Find the script in `bin/` 2. Understand its contract (args, output, exit codes) 3. Make the change — keep it idempotent 4. Update README if behaviour changed 5. Run `shellcheck` on the modified file --- ## Best Practices ### Idempotency Check before creating, use `>>` with grep guards, don't overwrite user configs. ### Error Handling ```bash set -euo pipefail command -v docker &>/dev/null || { echo "docker not found"; exit 1; } [[ -n "${1:-}" ]] || { echo "Usage: my-tool "; exit 1; } ``` ### Portability Targets **Debian** and **Ubuntu**. Use `apt`, assume bash at `/usr/bin/env bash`, check tools with `command -v`. ### Dry-run Support Scripts support `--dry-run`. Use the `run()` helper: ```bash run() { if [ "$DRY_RUN" -eq 1 ]; then log "(dry-run) $*" else "$@" fi } run sudo apt install -y git ``` ### Security - Never hardcode secrets — put them in `config/` (gitignored) - `chmod 600` for sensitive files - Validate input before shell commands - Use `sudo` only where needed ### Naming - CLI tools: `bin/pos--` - Legacy wrappers: `bin/wr-*` - App installers: `apps//.sh` - Lowercase with hyphens --- ## Working with Systemd Create `systemd/.service` — `postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically. ```ini [Unit] Description=My Service After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/your-script.sh Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target ``` --- ## Working with Config Files 1. Place the file in `config/` 2. Add copy logic to `postinstall.sh`: ```bash if [ -f config/your-config.conf ]; then mkdir -p "$HOME/.config/your-app" cp config/your-config.conf "$HOME/.config/your-app/your-config.conf" chmod 600 "$HOME/.config/your-app/your-config.conf" fi ``` --- ## Docker Compose / ScaleTail The installer clones [ScaleTail](https://github.com/tailscale-dev/ScaleTail) templates to `/usr/local/share/linux_post_install/scale-tail/` — 119+ self-hosted services with a Tailscale sidecar pattern. ### Deployed Layout ``` /usr/local/share/linux_post_install/scale-tail/ # Templates (git repo) └── services// ├── compose.yaml └── .env ~/.config/linux_post_install/compose.env # Global defaults /srv// # Active deployment ├── compose.yaml # Refreshed on update (preserves .env) ├── .env # Your config — preserved across updates ├── config/ └── data/ ``` ### Key Commands | Command | Behaviour | |---------|-----------| | `pos docker compose up ` | Deploys to `$SERVICES_BASE//`, creates `config/` + `data/`, generates `.env` | | `pos docker compose down ` | Stops the stack | | `pos docker compose update` | `git pull` templates + refreshes `compose.yaml` for all deployed services | | `pos docker compose config set K=V` | Sets global default in `~/.config/linux_post_install/compose.env` | ### .env Design - **Global config**: `~/.config/linux_post_install/compose.env` — one place for `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE` - **Per-service**: `//.env` — generated from template, filled from global config - **Updates**: `compose.yaml` refreshes from template but `.env` is never overwritten --- ## Commit Guidelines - Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` - Explain *why*, not just *what* - One logical change per commit ``` feat: add pos-disk-usage for monitoring disk space fix: pos-network-ip fails when no default route exists docs: add example output for pos-network-scan ``` --- ## Useful Commands ```bash # Syntax check a single script bash -n bin/my-script # ShellCheck linting shellcheck bin/my-script # Check all scripts for f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do bash -n "$f" || echo "FAIL: $f" done # Init submodule git submodule update --init # Pull latest ScaleTail templates git submodule update --remote compose/scale-tail # Test install in Docker docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash # inside: cd /repo && ./install.sh # Test app installers ./apps/install.sh ./apps/install.sh --all ./apps/install.sh docker vscode ```