diff --git a/AGENT_Context_Project.md b/AGENT_Context_Project.md index 15a080f..cb54417 100644 --- a/AGENT_Context_Project.md +++ b/AGENT_Context_Project.md @@ -78,6 +78,9 @@ Linux_post_install/ ├── completions/ │ └── pos.bash # Bash tab-completion for the pos CLI │ +├── config/ +│ └── authorized_keys # SSH public keys (gitignored) +│ ├── compose/ │ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services) │ @@ -266,8 +269,10 @@ ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar patter - `apps/install.sh` auto-discovers all `apps//*.sh` files (excluding itself) - Three modes: interactive (default), `--all`, or specific app names as arguments +- `--uninstall` switches to uninstall mode (same selection, invokes app scripts with `uninstall` argument) - Interactive TUI groups apps by category with section headers - Each app script is standalone, idempotent, sources `lib/common.sh` +- Every app script defines `install_()` **and** `uninstall_()`, dispatched via `case "${1:-}" in uninstall) ...` ### Installation Methods @@ -391,18 +396,18 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | File | Lines | Purpose | |------|-------|---------| -| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags | -| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban | -| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd | +| `install.sh` | 149 | Main orchestrator — 4 phases with CLI flags | +| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban | +| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd | | `lib/common.sh` | 121 | Shared library | -| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging | -| `bin/pos-docker-compose` | 317 | Largest script — full compose management | +| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging | +| `bin/pos-docker-compose` | 363 | Largest script — full compose management | | `bin/pos-system-firewall` | 284 | Interactive UFW manager | | `bin/pos-docker-ps` | 127 | Enhanced container overview | -| `bin/pos-docker-health` | ~90 | Quick health dashboard | -| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) | +| `bin/pos-docker-health` | 109 | Quick health dashboard | +| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) | | `completions/pos.bash` | 118 | Dynamic bash completion | -| `apps/install.sh` | 99 | App picker/orchestrator | +| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | --- diff --git a/DEV.md b/DEV.md index f6dfe04..1f7ca18 100644 --- a/DEV.md +++ b/DEV.md @@ -153,7 +153,16 @@ install_myapp() { spawn "Installing myapp" sudo apt install -y myapp } -install_myapp +uninstall_myapp() { + command -v myapp &>/dev/null || { log "myapp not installed"; return 0; } + spawn "Removing myapp" sudo apt purge -y myapp + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_myapp ;; + *) install_myapp ;; +esac ``` Place it in `apps//.sh`. It auto-appears in the picker — no registration needed. @@ -162,11 +171,14 @@ Place it in `apps//.sh`. It auto-appears in the picker — no re ### 2. Conventions -- Idempotent: check `command -v` before installing -- APT packages → `sudo apt install -y` inside `spawn` +- Idempotent: check `command -v` (or `flatpak list` / file existence) before installing **and** uninstalling +- Every app **must** provide an `uninstall_()` function and dispatch on `uninstall` via the `case` above — `apps/install.sh --uninstall` depends on it +- APT packages → `sudo apt install -y` inside `spawn`, remove with `sudo apt purge -y` + `sudo apt autoremove -y` +- Repo-based apps (apt repo added at install) → also remove the `.list` file and keyring in uninstall - Official scripts → `curl ... | sh` inside `spawn` -- Flatpak → `flatpak install -y flathub ` inside `spawn` +- Flatpak → `flatpak install -y flathub ` inside `spawn`, remove with `flatpak uninstall -y ` - `.deb` files → download to temp, `sudo apt install -y ./file.deb` inside `spawn` +- File/AppImage installs → remove the installed files, symlinks, and desktop entries in uninstall - `usermod` for groups → print re-login reminder --- diff --git a/README.md b/README.md index 069e745..3aa703c 100755 --- a/README.md +++ b/README.md @@ -173,7 +173,14 @@ These still work and forward to `pos`: `wr-ip`, `wr-checkport`, `wr-scan-ping`, ## Optional Apps -Install with `./apps/install.sh` (interactive), `./apps/install.sh --all`, or by name: +Install with `./apps/install.sh` (interactive), `./apps/install.sh --all`, or by name. +Uninstall the same way with `--uninstall`: + +```bash +./apps/install.sh --uninstall # interactive uninstall selection +./apps/install.sh --uninstall --all # uninstall everything +./apps/install.sh --uninstall brave vscode # uninstall specific apps +``` | Category | Apps | |----------|------| diff --git a/apps/browsers/brave.sh b/apps/browsers/brave.sh index b903966..499127f 100755 --- a/apps/browsers/brave.sh +++ b/apps/browsers/brave.sh @@ -15,4 +15,14 @@ install_brave() { spawn "Installing brave-browser" sudo apt install -y brave-browser } -install_brave +uninstall_brave() { + command -v brave-browser &>/dev/null || { log "brave not installed"; return 0; } + spawn "Removing brave-browser" sudo apt purge -y brave-browser + spawn "Cleaning up dependencies" sudo apt autoremove -y + spawn "Removing brave apt repo" sudo rm -f /etc/apt/sources.list.d/brave-browser-release.list /usr/share/keyrings/brave-browser-archive-keyring.gpg +} + +case "${1:-}" in + uninstall) uninstall_brave ;; + *) install_brave ;; +esac diff --git a/apps/development/opencode.sh b/apps/development/opencode.sh index 1792230..b0231db 100755 --- a/apps/development/opencode.sh +++ b/apps/development/opencode.sh @@ -13,4 +13,13 @@ install_opencode() { log "Add to PATH: export PATH=\"\$HOME/.opencode/bin:\$PATH\"" } -install_opencode +uninstall_opencode() { + [ -d "$HOME/.opencode" ] || { log "opencode not installed"; return 0; } + spawn "Removing opencode" rm -rf "$HOME/.opencode" + warn "Config/data remains in ~/.config/opencode — remove manually if desired" +} + +case "${1:-}" in + uninstall) uninstall_opencode ;; + *) install_opencode ;; +esac diff --git a/apps/development/vscode.sh b/apps/development/vscode.sh index 324741c..20ab74f 100755 --- a/apps/development/vscode.sh +++ b/apps/development/vscode.sh @@ -15,4 +15,14 @@ install_vscode() { spawn "Installing code" sudo apt install -y code } -install_vscode +uninstall_vscode() { + command -v code &>/dev/null || { log "vscode not installed"; return 0; } + spawn "Removing code" sudo apt purge -y code + spawn "Cleaning up dependencies" sudo apt autoremove -y + spawn "Removing vscode apt repo" sudo rm -f /etc/apt/sources.list.d/vscode.list /usr/share/keyrings/packages.microsoft.gpg +} + +case "${1:-}" in + uninstall) uninstall_vscode ;; + *) install_vscode ;; +esac diff --git a/apps/install.sh b/apps/install.sh index f7a99e8..c272f5d 100755 --- a/apps/install.sh +++ b/apps/install.sh @@ -6,26 +6,32 @@ usage() { cat </dev/null || { log "obs-studio not installed"; return 0; } + spawn "Removing obs-studio" sudo apt purge -y obs-studio + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_obs ;; + *) install_obs ;; +esac diff --git a/apps/media/scrcpy.sh b/apps/media/scrcpy.sh index 730db10..b124fba 100644 --- a/apps/media/scrcpy.sh +++ b/apps/media/scrcpy.sh @@ -50,4 +50,13 @@ EOF log "scrcpy $version installed (adb included in the bundle)" } -install_scrcpy +uninstall_scrcpy() { + command -v scrcpy &>/dev/null || { log "scrcpy not installed"; return 0; } + spawn "Removing scrcpy files" sudo rm -rf /usr/local/lib/scrcpy-* /usr/local/bin/scrcpy /usr/share/applications/scrcpy.desktop + log "scrcpy removed (bundled adb was removed with it)" +} + +case "${1:-}" in + uninstall) uninstall_scrcpy ;; + *) install_scrcpy ;; +esac diff --git a/apps/media/vlc.sh b/apps/media/vlc.sh index 0190924..1341781 100755 --- a/apps/media/vlc.sh +++ b/apps/media/vlc.sh @@ -7,4 +7,13 @@ install_vlc() { spawn "Installing vlc" sudo apt install -y vlc } -install_vlc +uninstall_vlc() { + command -v vlc &>/dev/null || { log "vlc not installed"; return 0; } + spawn "Removing vlc" sudo apt purge -y vlc + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_vlc ;; + *) install_vlc ;; +esac diff --git a/apps/networking/netbird.sh b/apps/networking/netbird.sh index 4283f9c..dbf59b6 100755 --- a/apps/networking/netbird.sh +++ b/apps/networking/netbird.sh @@ -11,4 +11,14 @@ install_netbird() { log "Join a network: sudo netbird up --setup-key " } -install_netbird +uninstall_netbird() { + command -v netbird &>/dev/null || { log "netbird not installed"; return 0; } + spawn "Removing netbird" sudo apt purge -y netbird + spawn "Cleaning up dependencies" sudo apt autoremove -y + spawn "Removing netbird apt repo" sudo rm -f /etc/apt/sources.list.d/netbird.list +} + +case "${1:-}" in + uninstall) uninstall_netbird ;; + *) install_netbird ;; +esac diff --git a/apps/networking/tailscale.sh b/apps/networking/tailscale.sh index cfcaca9..ed71de3 100755 --- a/apps/networking/tailscale.sh +++ b/apps/networking/tailscale.sh @@ -11,4 +11,14 @@ install_tailscale() { log "Start tailscale: sudo tailscale up" } -install_tailscale +uninstall_tailscale() { + command -v tailscale &>/dev/null || { log "tailscale not installed"; return 0; } + spawn "Removing tailscale" sudo apt purge -y tailscale + spawn "Cleaning up dependencies" sudo apt autoremove -y + spawn "Removing tailscale apt repo" sudo rm -f /etc/apt/sources.list.d/tailscale.list /usr/share/keyrings/tailscale-archive-keyring.gpg +} + +case "${1:-}" in + uninstall) uninstall_tailscale ;; + *) install_tailscale ;; +esac diff --git a/apps/networking/zerotier.sh b/apps/networking/zerotier.sh index fae674b..d3785ea 100755 --- a/apps/networking/zerotier.sh +++ b/apps/networking/zerotier.sh @@ -11,4 +11,14 @@ install_zerotier() { log "Join a network: sudo zerotier-cli join " } -install_zerotier +uninstall_zerotier() { + command -v zerotier-one &>/dev/null || { log "zerotier not installed"; return 0; } + spawn "Removing zerotier" sudo apt purge -y zerotier-one + spawn "Cleaning up dependencies" sudo apt autoremove -y + spawn "Removing zerotier apt repo" sudo rm -f /etc/apt/sources.list.d/zerotier.list +} + +case "${1:-}" in + uninstall) uninstall_zerotier ;; + *) install_zerotier ;; +esac diff --git a/apps/remote-access/termius.sh b/apps/remote-access/termius.sh index cd69799..678dd64 100755 --- a/apps/remote-access/termius.sh +++ b/apps/remote-access/termius.sh @@ -17,4 +17,13 @@ install_termius() { log "Termius installed — launch with 'termius'" } -install_termius +uninstall_termius() { + command -v termius &>/dev/null || { log "termius not installed"; return 0; } + spawn "Removing termius" sudo apt purge -y termius + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_termius ;; + *) install_termius ;; +esac diff --git a/apps/remote-access/vnc-viewer.sh b/apps/remote-access/vnc-viewer.sh index 9e8c522..777ed5f 100755 --- a/apps/remote-access/vnc-viewer.sh +++ b/apps/remote-access/vnc-viewer.sh @@ -7,4 +7,13 @@ install_vnc_viewer() { spawn "Installing tigervnc-viewer" sudo apt install -y tigervnc-viewer } -install_vnc_viewer +uninstall_vnc_viewer() { + command -v vncviewer &>/dev/null || { log "tigervnc-viewer not installed"; return 0; } + spawn "Removing tigervnc-viewer" sudo apt purge -y tigervnc-viewer + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_vnc_viewer ;; + *) install_vnc_viewer ;; +esac diff --git a/apps/system/docker.sh b/apps/system/docker.sh index dd5636b..24ea465 100755 --- a/apps/system/docker.sh +++ b/apps/system/docker.sh @@ -12,4 +12,23 @@ install_docker() { warn "Log out and back in for docker group to take effect" } -install_docker +uninstall_docker() { + command -v docker &>/dev/null || { log "docker not installed"; return 0; } + + local pkgs=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-compose-v2) + local installed=() + for p in "${pkgs[@]}"; do + dpkg -s "$p" &>/dev/null && installed+=("$p") + done + if [ "${#installed[@]}" -gt 0 ]; then + spawn "Removing docker engine" sudo apt purge -y "${installed[@]}" + spawn "Cleaning up dependencies" sudo apt autoremove -y + fi + spawn "Removing docker apt repo" sudo rm -f /etc/apt/sources.list.d/docker.list /etc/apt/keyrings/docker.asc /etc/apt/keyrings/docker.gpg + warn "Docker data remains in /var/lib/docker — remove manually if desired" +} + +case "${1:-}" in + uninstall) uninstall_docker ;; + *) install_docker ;; +esac diff --git a/apps/system/qemu.sh b/apps/system/qemu.sh index bb63c08..a0b1875 100755 --- a/apps/system/qemu.sh +++ b/apps/system/qemu.sh @@ -15,4 +15,16 @@ install_qemu() { warn "Log out and back in for libvirt/kvm groups to take effect" } -install_qemu +uninstall_qemu() { + command -v qemu-system-x86_64 &>/dev/null || { log "qemu not installed"; return 0; } + spawn "Removing qemu and libvirt" sudo apt purge -y \ + qemu-system qemu-utils qemu-kvm \ + libvirt-daemon-system libvirt-clients \ + bridge-utils virt-manager + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_qemu ;; + *) install_qemu ;; +esac diff --git a/apps/utilities/affine.sh b/apps/utilities/affine.sh index 93056bd..13cab50 100755 --- a/apps/utilities/affine.sh +++ b/apps/utilities/affine.sh @@ -32,4 +32,12 @@ EOF " } -install_affine +uninstall_affine() { + command -v affine &>/dev/null || { log "affine not installed"; return 0; } + spawn "Removing AFFiNE" sudo rm -rf /opt/affine /usr/local/bin/affine /usr/share/applications/affine.desktop +} + +case "${1:-}" in + uninstall) uninstall_affine ;; + *) install_affine ;; +esac diff --git a/apps/utilities/btop.sh b/apps/utilities/btop.sh index f28a049..cf433cc 100755 --- a/apps/utilities/btop.sh +++ b/apps/utilities/btop.sh @@ -7,4 +7,13 @@ install_btop() { spawn "Installing btop" sudo apt install -y btop } -install_btop +uninstall_btop() { + command -v btop &>/dev/null || { log "btop not installed"; return 0; } + spawn "Removing btop" sudo apt purge -y btop + spawn "Cleaning up dependencies" sudo apt autoremove -y +} + +case "${1:-}" in + uninstall) uninstall_btop ;; + *) install_btop ;; +esac diff --git a/apps/utilities/localsend.sh b/apps/utilities/localsend.sh index 050a36b..05fec80 100755 --- a/apps/utilities/localsend.sh +++ b/apps/utilities/localsend.sh @@ -16,4 +16,15 @@ install_localsend() { spawn "Installing localsend" sudo flatpak install -y flathub org.localsend.localsend_app } -install_localsend +uninstall_localsend() { + if ! flatpak list 2>/dev/null | grep -q org.localsend.localsend_app; then + log "localsend not installed" + return 0 + fi + spawn "Removing localsend" sudo flatpak uninstall -y org.localsend.localsend_app +} + +case "${1:-}" in + uninstall) uninstall_localsend ;; + *) install_localsend ;; +esac diff --git a/install.sh b/install.sh index dd436ac..021d2ad 100755 --- a/install.sh +++ b/install.sh @@ -34,6 +34,11 @@ Options: (1=preinstall, 2=scripts, 3=postinstall, 4=scalepoint) --no-color Disable colored output -h, --help Show this help message + +Uninstall optional apps later with: + ./apps/install.sh --uninstall # interactive uninstall selection + ./apps/install.sh --uninstall --all # uninstall everything + ./apps/install.sh --uninstall ... # uninstall specific apps EOF exit 0 } diff --git a/session b/session new file mode 100644 index 0000000..be15ec8 --- /dev/null +++ b/session @@ -0,0 +1,1249 @@ +{ + "info": { + "id": "ses_0509eda1dffe8QPzn800kHRa9l", + "slug": "tidy-river", + "projectID": "5cfe7639febf485780deaff12efb2e89108cbf30", + "directory": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "path": "", + "title": "Repository context documentation", + "agent": "plan", + "model": { + "id": "big-pickle", + "providerID": "opencode", + "variant": "default" + }, + "version": "1.17.18", + "summary": { + "additions": 0, + "deletions": 0, + "files": 0 + }, + "cost": 0, + "tokens": { + "input": 65035, + "output": 2329, + "reasoning": 2930, + "cache": { + "read": 196224, + "write": 0 + } + }, + "time": { + "created": 1785353807330, + "updated": 1785354113960 + } + }, + "messages": [ + { + "info": { + "role": "user", + "time": { + "created": 1785353807395 + }, + "agent": "plan", + "model": { + "providerID": "opencode", + "modelID": "big-pickle" + }, + "summary": { + "diffs": [] + }, + "id": "msg_faf612623001UNz7GERJtr7i46", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "type": "text", + "text": "you are now founded for this repo, you are will known every single line in this project so create doc or use agent context doc to help you for be refernce of your knowledge of this repo project ", + "id": "prt_faf61262a001Pmtk4MmJ4QRO8l", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf612623001UNz7GERJtr7i46" + } + ] + }, + { + "info": { + "parentID": "msg_faf612623001UNz7GERJtr7i46", + "role": "assistant", + "mode": "plan", + "agent": "plan", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 8728, + "input": 8412, + "output": 258, + "reasoning": 58, + "cache": { + "write": 0, + "read": 0 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353807461, + "completed": 1785353907141 + }, + "finish": "tool-calls", + "id": "msg_faf612665002qZ8zYE85FYpCV3", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-start", + "id": "prt_faf613fd6001JhhA7WbmCkVrNh", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf612665002qZ8zYE85FYpCV3" + }, + { + "type": "reasoning", + "text": "The user wants me to create a documentation or agent context document for this repository to serve as a reference for my knowledge of the project. Let me first explore the repository structure to understand what this project is about, then create a comprehensive context document.\n\nLet me start by exploring the repository structure.", + "time": { + "start": 1785353814603, + "end": 1785353815045 + }, + "id": "prt_faf61424b0015ydyHamtBCXl9o", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf612665002qZ8zYE85FYpCV3" + }, + { + "type": "tool", + "tool": "task", + "callID": "call_00_nzB1VokQjgweLakU3C0v2161", + "state": { + "status": "completed", + "input": { + "description": "Explore repo structure", + "prompt": "Explore the repository at /mnt/hdd/ciya_space/work/repo/Linux_post_install thoroughly. I need you to:\n\n1. List the top-level directory structure\n2. Read all configuration files (like .json, .yaml, .toml, .cfg, .ini, Makefile, etc.)\n3. Read all scripts (.sh, .py, etc.)\n4. Read any documentation files (.md, .txt, etc.)\n5. Understand the purpose of the project\n6. Identify all key files, their locations, and what they do\n\nReturn a comprehensive summary of:\n- Project purpose and overview\n- Directory tree structure\n- List of all files with brief descriptions\n- Key configuration details\n- Entry points and main scripts\n- Any patterns, naming conventions, or important constants\n\nBe very thorough - read every single file in the repository.", + "subagent_type": "explore" + }, + "output": "\n\nI have now read every file in the repository. Here is the comprehensive summary:\n\n---\n\n# Comprehensive Repository Analysis: `Linux_post_install`\n\n## Project Purpose & Overview\n\n**Linux_post_install** is a personal bootstrap and homelab toolkit for Debian/Ubuntu systems. It automates post-installation setup -- turning a bare Linux install into a fully productive machine with a single command. The project provides:\n\n- **Automated system package installation** (25+ essential packages via `apt`)\n- **A unified CLI called `pos`** for network, Docker, media, system, and SSH tasks, installed to `/usr/local/bin/`\n- **15 optional desktop app installers** across 7 categories (browsers, development, media, networking, remote access, system, utilities)\n- **Docker Compose service management** via ScaleTail templates (119+ self-hosted services with Tailscale sidecar networking)\n- **systemd service management** for SSH agent and boot-time automation\n- **Bash completion** for the `pos` CLI\n- **Logging** -- all non-interactive commands log to `~/.local/share/linux_post_install/logs/`\n- **Legacy wrappers** for backward compatibility (`wr-ip`, `mp3`, `vbox`, etc.)\n\n**Repository:** `https://gitea.skink-platy.ts.net/admin/Linux_post_install`\n**Target OS:** Debian / Ubuntu (uses `apt`)\n**Shell:** Bash (`#!/usr/bin/env bash`)\n\n---\n\n## Complete Directory Tree Structure\n\n```\nLinux_post_install/\n├── .git/ # Git repository data\n├── .gitignore # Gitignore rules\n├── .gitmodules # Git submodule definition\n├── AGENT_Context_Project.md # AI agent context document (426 lines)\n├── README.md # User-facing documentation (192 lines)\n├── DEV.md # Developer guide (364 lines)\n├── algorithm.md # Algorithm diagrams (352 lines)\n├── install.sh # Main orchestrator / entry point (144 lines)\n├── preinstall.sh # Phase 1: system packages (52 lines)\n├── postinstall.sh # Phase 3: user config + services (90 lines)\n│\n├── lib/\n│ └── common.sh # Shared library (colors, logging, spinners, timers) (121 lines)\n│\n├── bin/ # CLI tools -- installed to /usr/local/bin/\n│ ├── pos # Main CLI dispatcher with smart arg matching + logging (145 lines)\n│ ├── pos-network-ip # Show interfaces, routes, public IP (46 lines)\n│ ├── pos-network-checkport # TCP port connectivity checker (44 lines)\n│ ├── pos-network-scan # Two-phase parallel ping sweep via nmap (270 lines)\n│ ├── pos-docker-ps # Enhanced docker ps (health, IPs, ports, uptime) (127 lines)\n│ ├── pos-docker-health # Quick health dashboard, exits 1 if unhealthy (109 lines)\n│ ├── pos-docker-compose # Full Docker Compose service manager (363 lines)\n│ ├── pos-media-mp3 # Audio downloader via yt-dlp -> MP3 (30 lines)\n│ ├── pos-media-mp4 # Video downloader with interactive format select (33 lines)\n│ ├── pos-system-firewall # Interactive UFW manager, menu-driven (284 lines)\n│ ├── pos-ssh-load-keys # Load SSH keys into ssh-agent (30 lines)\n│ ├── pos-vbox # Disposable Docker-based \"VMs\" (156 lines)\n│ ├── autostart.sh # Boot-time network check script (14 lines)\n│ ├── wr-ip # Legacy wrapper -> pos network ip (2 lines)\n│ ├── wr-checkport # Legacy wrapper -> pos network checkport (2 lines)\n│ ├── wr-scan-ping # Legacy wrapper -> pos network scan (2 lines)\n│ ├── wr-docker # Legacy wrapper -> pos docker ps (2 lines)\n│ ├── wr-compose # Legacy wrapper -> pos docker compose (2 lines)\n│ ├── wr-ufw # Legacy wrapper -> pos system firewall (2 lines)\n│ ├── mp3 # Legacy wrapper -> pos media mp3 (2 lines)\n│ ├── mp4 # Legacy wrapper -> pos media mp4 (2 lines)\n│ ├── vbox # Legacy wrapper -> pos vbox (2 lines)\n│ └── ssh-load-all # Legacy wrapper -> pos ssh load-keys (2 lines)\n│\n├── apps/ # Optional desktop app installers\n│ ├── install.sh # Interactive picker / orchestrator (143 lines)\n│ ├── browsers/\n│ │ └── brave.sh # Brave Browser via APT repo (18 lines)\n│ ├── development/\n│ │ ├── opencode.sh # opencode AI agent via official script (16 lines)\n│ │ └── vscode.sh # VS Code via Microsoft APT repo (18 lines)\n│ ├── media/\n│ │ ├── obs.sh # OBS Studio via apt (10 lines)\n│ │ ├── scrcpy.sh # scrcpy Android mirror via GitHub release (53 lines)\n│ │ └── vlc.sh # VLC via apt (10 lines)\n│ ├── networking/\n│ │ ├── netbird.sh # NetBird VPN via official script (14 lines)\n│ │ ├── tailscale.sh # Tailscale VPN via official script (14 lines)\n│ │ └── zerotier.sh # ZeroTier VPN via official script (14 lines)\n│ ├── remote-access/\n│ │ ├── termius.sh # Termius SSH client via .deb (20 lines)\n│ │ └── vnc-viewer.sh # TigerVNC Viewer via apt (10 lines)\n│ ├── system/\n│ │ ├── docker.sh # Docker Engine via get.docker.com (15 lines)\n│ │ └── qemu.sh # QEMU + libvirt + KVM via apt (18 lines)\n│ └── utilities/\n│ ├── affine.sh # AFFiNE knowledge base via AppImage (35 lines)\n│ ├── btop.sh # btop resource monitor via apt (10 lines)\n│ └── localsend.sh # LocalSend via Flatpak (19 lines)\n│\n├── completions/\n│ └── pos.bash # Dynamic bash tab-completion for pos CLI (118 lines)\n│\n├── config/\n│ └── authorized_keys # SSH public keys (1 line, gitignored)\n│\n├── systemd/\n│ ├── ssh-agent.service # System-wide SSH agent socket (14 lines)\n│ └── autostart.service # Runs autostart.sh on boot (13 lines)\n│\n└── compose/ # (git submodule, not cloned locally)\n └── scale-tail/ # ScaleTail templates (119+ Docker Compose services)\n```\n\n**Total: 43 files** (excluding `.git/` internals and the uninitialized submodule)\n\n---\n\n## Key Configuration Details\n\n### Installation Phases (orchestrated by `install.sh`)\n\n| Phase | Script | Responsibility | Requires Root |\n|-------|--------|---------------|---------------|\n| 1 | `preinstall.sh` | `apt update`, install 25+ packages, yt-dlp, fail2ban | Yes |\n| 2 | `install.sh` (self) | Copy `bin/*` -> `/usr/local/bin/` (chmod 755), `lib/common.sh` -> `/usr/local/bin/common.sh` (chmod 644) | Yes |\n| 3 | `postinstall.sh` | fail2ban SSH jail config, PATH in `~/.bashrc`, bash completion, systemd services, SSH authorized keys, rclone config | No (runs as user) |\n| 4 | `install.sh` (self) | Shallow-clone ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail/` | Yes |\n| 5 (opt) | `apps/install.sh` | Interactive app picker or `--all` | Some apps need sudo |\n\n### `install.sh` Flags\n\n| Flag | Purpose |\n|------|---------|\n| `--apps` | Run interactive app picker after core install |\n| `--full` | Core install + all apps (non-interactive) |\n| `--dry-run` | Preview without executing anything |\n| `--skip ` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |\n| `--steps ` | Run only specific phases, e.g. `--steps 1,3` or `--steps 1-3` |\n| `--no-color` | Disable colored output |\n\n### Key Config Paths (runtime)\n\n| Path | Purpose |\n|------|---------|\n| `~/.config/linux_post_install/compose.env` | Global Docker Compose defaults (TS_AUTHKEY, TZ, DNS_SERVER, SERVICES_BASE) |\n| `/usr/local/share/linux_post_install/scale-tail/services/` | ScaleTail template directory |\n| `/srv//` | Per-service deployment directory (configurable via `SERVICES_BASE`) |\n| `~/.local/share/linux_post_install/logs/` | Command logs |\n| `~/.bashrc` | Modified by postinstall to add PATH and bash completion |\n| `~/.ssh/authorized_keys` | SSH keys installed from `config/authorized_keys` |\n| `~/.config/rclone/rclone.conf` | rclone config (if `config/rclone.conf` exists) |\n\n### Global Compose Config Keys\n\n| Key | Required | Default | Purpose |\n|-----|----------|---------|---------|\n| `TS_AUTHKEY` | Yes | -- | Tailscale auth key for sidecar networking |\n| `TZ` | No | `Europe/Amsterdam` | Timezone for services |\n| `DNS_SERVER` | No | `9.9.9.9` | Custom DNS server |\n| `SERVICES_BASE` | No | `/srv` | Root directory for all deployments |\n\n### Config Cascade (Three Layers for Docker Compose)\n\n```\n1. Template .env (per-service defaults from ScaleTail, read-only)\n ↓\n2. Global config (~/.config/linux_post_install/compose.env, user-edited)\n ↓\n3. Per-service .env (/srv//.env -- created on first deploy, NEVER overwritten)\n```\n\n### System Packages Installed by `preinstall.sh`\n\n`git curl wget vim nano tmux tree jq unzip zip rsync htop btop telnet net-tools iputils-ping traceroute tcpdump nmap openssh-client openssh-server ufw fail2ban ca-certificates gnupg lsb-release python3 python3-pip rclone` (25 packages) + yt-dlp.\n\n### systemd Services\n\n| Service | File | Purpose |\n|---------|------|---------|\n| `ssh-agent.service` | `systemd/ssh-agent.service` | System-wide SSH agent, socket at `/run/ssh-agent/socket` |\n| `autostart.service` | `systemd/autostart.service` | Runs `autostart.sh` on boot (network check) |\n\n### SSH Keys\n- Public keys stored in `config/authorized_keys` (gitignored, contains 1 ED25519 key)\n- Installed to `~/.ssh/authorized_keys` by `postinstall.sh`\n\n---\n\n## Entry Points and Main Scripts\n\n### Primary Entry Point: `install.sh`\n\nThe main orchestrator at `/mnt/hdd/ciya_space/work/repo/Linux_post_install/install.sh`. It:\n1. Parses CLI flags (`--apps`, `--full`, `--dry-run`, `--skip`, `--steps`, `--no-color`)\n2. Runs phases 1-4 sequentially based on flags\n3. Optionally runs `apps/install.sh` if `--apps` or `--full`\n\n### Secondary Entry Points\n\n| Script | How to invoke | Purpose |\n|--------|--------------|---------|\n| `./install.sh` | `bash install.sh` | Full bootstrap (core) |\n| `./install.sh --apps` | `bash install.sh --apps` | Core + interactive app picker |\n| `./install.sh --full` | `bash install.sh --full` | Core + all apps |\n| `bash apps/install.sh` | Direct call | App installer standalone |\n| `pos` (after install) | `pos ` | Main daily-use CLI |\n\n### The `pos` CLI Dispatch Mechanism\n\n`bin/pos` uses **variable-length argument matching**:\n1. Scans its directory for executables named `pos-*`\n2. For command `pos docker compose up jellyfin`, it tries candidates:\n - `pos-docker-compose-up-jellyfin` (not found)\n - `pos-docker-compose-up` (not found)\n - `pos-docker-compose` (found! runs with args `up jellyfin`)\n3. Interactive commands (`system-firewall`, `media-mp4`) only log invocation; all others log full output to timestamped files\n\n### Available `pos` Commands\n\n| Category | Command | Script | Description |\n|----------|---------|--------|-------------|\n| network | ip | `pos-network-ip` | Show interfaces, routes, public IP |\n| network | checkport | `pos-network-checkport` | Check TCP port connectivity |\n| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |\n| docker | ps | `pos-docker-ps` | Enhanced container overview |\n| docker | health | `pos-docker-health` | Health dashboard (exits 1 if unhealthy) |\n| docker | compose | `pos-docker-compose` | Service manager (ls/up/down/restart/logs/update/config) |\n| media | mp3 | `pos-media-mp3` | Download audio as MP3 |\n| media | mp4 | `pos-media-mp4` | Download video with format select |\n| system | firewall | `pos-system-firewall` | Interactive UFW management |\n| ssh | load-keys | `pos-ssh-load-keys` | Load SSH keys into agent |\n| vbox | create/enter/ls/start/stop/rm | `pos-vbox` | Disposable Docker containers as VMs |\n\n---\n\n## Patterns, Naming Conventions, and Important Constants\n\n### Naming Conventions\n\n| Pattern | Example | Purpose |\n|---------|---------|---------|\n| `pos--` | `pos-network-ip` | Canonical CLI tool names |\n| `wr-*` | `wr-ip`, `wr-compose` | Legacy wrappers that forward to `pos` |\n| `apps//.sh` | `apps/browsers/brave.sh` | Optional app installers |\n| `systemd/.service` | `systemd/ssh-agent.service` | systemd unit files |\n| `completions/.bash` | `completions/pos.bash` | Bash completion files |\n\n### Script Standards\n\n- **Shebang:** `#!/usr/bin/env bash`\n- **Strict mode:** `set -euo pipefail` (all scripts)\n- **Help:** Every script accepts `-h`/`--help` via `case` pattern\n- **Idempotency:** Check `command -v` before installing, use grep guards before appending\n- **Exit codes:** 0 = success, 1 = error\n- **Shared library source pattern:** `source \"$(dirname \"$0\")/../lib/common.sh\"`\n- **Fallback pattern for shared lib:** `source \"$(dirname \"$0\")/../lib/common.sh\" 2>/dev/null || source \"$(dirname \"$0\")/common.sh\"`\n\n### Shared Library (`lib/common.sh`) Functions\n\n| Function | Purpose |\n|----------|---------|\n| `log \"msg\"` | Green `[+]` status message |\n| `warn \"msg\"` | Yellow `[!]` warning |\n| `err \"msg\"` | Red `ERROR:` + exit 1 |\n| `ok \"msg\"` | Green `OK` prefix |\n| `section \"title\"` | Cyan-bordered section header |\n| `step N T \"msg\"` | Numbered step header |\n| `run cmd` | Executes command, respects `$DRY_RUN` |\n| `spawn \"msg\" cmd` | Animated braille spinner + elapsed time + OK/FAIL |\n| `timer_start` / `timer_stop` | Elapsed time tracking |\n| `confirm \"prompt\" [default]` | y/N or Y/n prompt |\n\n### Scripts that do NOT source `common.sh` (self-contained)\n- `bin/pos` (main dispatcher -- has its own color definitions)\n- `bin/pos-network-ip`\n- `bin/pos-network-checkport`\n- `bin/pos-network-scan`\n- `bin/pos-media-mp3`\n- `bin/pos-media-mp4`\n- `bin/pos-ssh-load-keys`\n- `bin/pos-system-firewall` (has its own inline `log`/`warn`/`err`)\n\n### Logging System (in `bin/pos`)\n\n- **Log dir:** `~/.local/share/linux_post_install/logs/`\n- **Per-command log:** `YYYYMMDD_HHMMSS_pos_.log` (full stdout+stderr via `tee`)\n- **Main log:** `pos.log` (timestamp + command + logfile + exit code appended per invocation)\n- **Interactive commands** (`system-firewall`, `media-mp4`): only log invocation, not output\n\n### Docker Compose Global Config File (`compose.env`)\nLocated at `~/.config/linux_post_install/compose.env`. Managed by `pos-docker-compose`:\n- `config show` -- display current config\n- `config set K=V` -- set a key\n- `config edit` -- open in `$EDITOR` (defaults to nano)\n\n### Default Global Config Values (in code)\n- `SERVICES_BASE=/srv`\n- `TZ=Europe/Amsterdam`\n- `DNS_SERVER=9.9.9.9`\n\n### Important Directories (post-install)\n\n| Path | Purpose |\n|------|---------|\n| `/usr/local/bin/` | All `bin/*` scripts installed here |\n| `/usr/local/bin/common.sh` | Shared library (chmod 644) |\n| `/usr/local/share/linux_post_install/scale-tail/` | ScaleTail templates |\n| `/usr/local/share/bash-completion/completions/pos.bash` | Bash completion file |\n\n### Git Submodule\n- **Path:** `compose/scale-tail`\n- **URL:** `https://github.com/tailscale-dev/ScaleTail.git`\n- **Status:** Not initialized in this checkout (no `.git` directory inside)\n\n### .gitignore Highlights\n- Ignores `__pycache__/`, `*.pyc`, `dist/`, `build/`, `*.spec`\n- Ignores editor swap files (`*.swp`, `*.swo`, `*~`)\n- Ignores OS files (`.DS_Store`, `Thumbs.db`)\n- Ignores Docker Compose user data: `compose/custom/`, `compose/config.env`\n\n### Security Practices\n- SSH keys and rclone config in `config/` are gitignored\n- Sensitive files get `chmod 600`\n- Never hardcode secrets in scripts\n- Input validation before shell commands\n- `sudo` used only where necessary\n- `confirm()` prompts for destructive actions\n\n### App Installation Methods (across 15 apps)\n\n| Method | Apps |\n|--------|------|\n| `apt install` | btop, OBS, VLC, VNC Viewer, QEMU |\n| APT repo (GPG key + apt source) | Brave, VS Code |\n| Official `curl | sh` | Docker, Tailscale, NetBird, ZeroTier, opencode |\n| AppImage | AFFiNE |\n| GitHub release binary | scrcpy |\n| Flatpak | LocalSend |\n| .deb package | Termius |\n\n### Commit Guidelines (from DEV.md)\n- Conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`\n- Explain **why**, not just **what**\n- One logical change per commit\n\n### Key Constants\n- `INTERACTIVE_CMDS=\"system-firewall media-mp4\"` (in `bin/pos`, line 112)\n- `SCALE_DIR=\"/usr/local/share/linux_post_install/scale-tail/services\"` (in `pos-docker-compose`, line 5)\n- `CONFIG_ENV=\"${HOME}/.config/linux_post_install/compose.env\"` (in `pos-docker-compose`, line 6)\n- VBox label: `linux_post_install.vbox=true` (in `pos-vbox`, line 96)\n- Default VBox image: `ubuntu:22.04` (in `pos-vbox`, line 59)\n\n---\n\n## Quick Reference: All Files\n\n| # | File Path | Lines | Type | Description |\n|---|-----------|-------|------|-------------|\n| 1 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/.gitignore` | 21 | Config | Gitignore rules |\n| 2 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/.gitmodules` | 3 | Config | Git submodule (ScaleTail) |\n| 3 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/README.md` | 192 | Doc | User-facing documentation |\n| 4 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/DEV.md` | 364 | Doc | Developer guide |\n| 5 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md` | 426 | Doc | AI agent context document |\n| 6 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/algorithm.md` | 352 | Doc | Algorithm/flow diagrams |\n| 7 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/install.sh` | 144 | Script | Main orchestrator / entry point |\n| 8 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/preinstall.sh` | 52 | Script | System packages installer |\n| 9 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/postinstall.sh` | 90 | Script | User config + services |\n| 10 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/lib/common.sh` | 121 | Script | Shared library (colors/logging/spinners) |\n| 11 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos` | 145 | Script | Main CLI dispatcher |\n| 12 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-network-ip` | 46 | Script | Network IP information |\n| 13 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-network-checkport` | 44 | Script | TCP port checker |\n| 14 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-network-scan` | 270 | Script | Network scan (nmap) |\n| 15 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-docker-ps` | 127 | Script | Enhanced docker ps |\n| 16 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-docker-health` | 109 | Script | Docker health dashboard |\n| 17 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-docker-compose` | 363 | Script | Docker Compose service manager |\n| 18 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-media-mp3` | 30 | Script | Audio downloader (yt-dlp) |\n| 19 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-media-mp4` | 33 | Script | Video downloader (yt-dlp) |\n| 20 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-system-firewall` | 284 | Script | Interactive UFW manager |\n| 21 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-ssh-load-keys` | 30 | Script | SSH key loader |\n| 22 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/pos-vbox` | 156 | Script | Docker-based VMs |\n| 23 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/autostart.sh` | 14 | Script | Boot-time network check |\n| 24 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/wr-ip` | 2 | Script | Legacy wrapper |\n| 25 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/wr-checkport` | 2 | Script | Legacy wrapper |\n| 26 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/wr-scan-ping` | 2 | Script | Legacy wrapper |\n| 27 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/wr-docker` | 2 | Script | Legacy wrapper |\n| 28 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/wr-compose` | 2 | Script | Legacy wrapper |\n| 29 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/wr-ufw` | 2 | Script | Legacy wrapper |\n| 30 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/mp3` | 2 | Script | Legacy wrapper |\n| 31 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/mp4` | 2 | Script | Legacy wrapper |\n| 32 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/vbox` | 2 | Script | Legacy wrapper |\n| 33 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/bin/ssh-load-all` | 2 | Script | Legacy wrapper |\n| 34 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/completions/pos.bash` | 118 | Script | Bash completion for pos |\n| 35 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/config/authorized_keys` | 1 | Config | SSH public key |\n| 36 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/systemd/ssh-agent.service` | 14 | Config | SSH agent systemd unit |\n| 37 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/systemd/autostart.service` | 13 | Config | Autostart systemd unit |\n| 38 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/install.sh` | 143 | Script | App installer orchestrator |\n| 39 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/browsers/brave.sh` | 18 | Script | Brave browser installer |\n| 40 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/development/opencode.sh` | 16 | Script | opencode AI agent installer |\n| 41 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/development/vscode.sh` | 18 | Script | VS Code installer |\n| 42 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/media/obs.sh` | 10 | Script | OBS Studio installer |\n| 43 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/media/scrcpy.sh` | 53 | Script | scrcpy Android mirror installer |\n| 44 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/media/vlc.sh` | 10 | Script | VLC installer |\n| 45 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/networking/netbird.sh` | 14 | Script | NetBird VPN installer |\n| 46 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/networking/tailscale.sh` | 14 | Script | Tailscale VPN installer |\n| 47 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/networking/zerotier.sh` | 14 | Script | ZeroTier VPN installer |\n| 48 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/remote-access/termius.sh` | 20 | Script | Termius SSH client installer |\n| 49 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/remote-access/vnc-viewer.sh` | 10 | Script | TigerVNC viewer installer |\n| 50 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/system/docker.sh` | 15 | Script | Docker Engine installer |\n| 51 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/system/qemu.sh` | 18 | Script | QEMU/KVM installer |\n| 52 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/utilities/affine.sh` | 35 | Script | AFFiNE knowledge base installer |\n| 53 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/utilities/btop.sh` | 10 | Script | btop resource monitor installer |\n| 54 | `/mnt/hdd/ciya_space/work/repo/Linux_post_install/apps/utilities/localsend.sh` | 19 | Script | LocalSend file sharing installer |\n\n", + "metadata": { + "parentSessionId": "ses_0509eda1dffe8QPzn800kHRa9l", + "sessionId": "ses_0509eb5e8ffe3i1oUlujBrMEIf", + "model": { + "modelID": "big-pickle", + "providerID": "opencode" + }, + "truncated": false + }, + "title": "Explore repo structure", + "time": { + "start": 1785353816612, + "end": 1785353905509 + } + }, + "id": "prt_faf61440e00147C1dcbjWpsUXG", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf612665002qZ8zYE85FYpCV3" + }, + { + "reason": "tool-calls", + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-finish", + "tokens": { + "total": 8728, + "input": 8412, + "output": 258, + "reasoning": 58, + "cache": { + "write": 0, + "read": 0 + } + }, + "cost": 0, + "id": "prt_faf62a8b2001ysrPKzl8R0bcim", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf612665002qZ8zYE85FYpCV3" + } + ] + }, + { + "info": { + "parentID": "msg_faf612623001UNz7GERJtr7i46", + "role": "assistant", + "mode": "plan", + "agent": "plan", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 16072, + "input": 6901, + "output": 215, + "reasoning": 252, + "cache": { + "write": 0, + "read": 8704 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353907150, + "completed": 1785353914652 + }, + "finish": "tool-calls", + "id": "msg_faf62abce001vJqgMnbuKhau56", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-start", + "id": "prt_faf62b3bc001jKEP7n025D1qWe", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + }, + { + "type": "reasoning", + "text": "The user wants me to create a documentation or agent context document for this repo. They already have an `AGENT_Context_Project.md` file. Let me read the existing one to see what's there, and then the README.md and DEV.md to understand the full picture. Then I'll update or enhance the context document as needed.\n\nWait, the user said \"create doc or use agent context doc to help you for be reference of your knowledge of this repo project\". So they want me to use the agent context document as a reference. But I'm in plan mode - I can't make edits.\n\nLet me re-read their request. They said \"you are now founded for this repo, you are will known every single line in this project so create doc or use agent context doc to help you for be refernce of your knowledge of this repo project\"\n\nThis is a bit unclear but I think they want me to:\n1. Create or use the AGENT_Context_Project.md file as a reference\n2. So that I can be knowledgeable about the project\n\nSince I'm in plan mode, I should read the existing context document, understand what's there, and perhaps propose any improvements. Let me read the existing files first.", + "time": { + "start": 1785353909722, + "end": 1785353911983 + }, + "id": "prt_faf62b5da0015Tf6SvOyZc7my5", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + }, + { + "type": "tool", + "tool": "read", + "callID": "call_00_x7NL3OQG2c7hlJ7UCie88385", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md" + }, + "output": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\nfile\n\n1: # AGENT Context — Linux_post_install Project\n2: \n3: > **Purpose:** Single-source context document so any AI agent can understand the project, navigate the codebase, and make correct contributions.\n4: \n5: ---\n6: \n7: ## 1. Project Overview\n8: \n9: **Linux_post_install** is a personal bootstrap and homelab toolkit for Debian/Ubuntu. One command turns a bare install into a fully productive machine:\n10: \n11: - Automated system package installation (25+ packages)\n12: - A unified CLI (`pos`) for network, Docker, media, system, and SSH tasks\n13: - Optional desktop application installers (15 apps)\n14: - Docker Compose service management via ScaleTail templates (119+ self-hosted services with Tailscale sidecar)\n15: - Systemd service management for boot-time automation\n16: \n17: **Repository:** `https://gitea.skink-platy.ts.net/admin/Linux_post_install`\n18: **Target OS:** Debian / Ubuntu (uses `apt`)\n19: **Shell:** Bash (`#!/usr/bin/env bash`)\n20: \n21: ---\n22: \n23: ## 2. Directory Structure\n24: \n25: ```\n26: Linux_post_install/\n27: ├── install.sh # Main orchestrator — entry point\n28: ├── preinstall.sh # Phase 1: system packages via apt + yt-dlp\n29: ├── postinstall.sh # Phase 3: PATH, bash completion, systemd services\n30: │\n31: ├── lib/\n32: │ └── common.sh # Shared library (colors, logging, spinner, timer, run)\n33: │\n34: ├── bin/ # CLI tools — installed to /usr/local/bin/\n35: │ ├── pos # Main dispatcher — smart arg matching to pos-* scripts\n36: │ ├── pos-network-ip # Show interfaces, routes, public IP\n37: │ ├── pos-network-checkport # TCP port checker\n38: │ ├── pos-network-scan # Parallel ping sweep of CIDR subnet\n39: │ ├── pos-docker-ps # Enhanced docker ps (health, IPs, ports, uptime)\n40: │ ├── pos-docker-health # Quick one-glance health dashboard\n41: │ ├── pos-docker-compose # Docker Compose service manager (largest script, 317 lines)\n42: │ ├── pos-media-mp3 # Audio downloader (yt-dlp → MP3)\n43: │ ├── pos-media-mp4 # Video downloader (yt-dlp → MP4, interactive format select)\n44: │ ├── pos-system-firewall # Interactive UFW manager (menu-driven, 284 lines)\n45: │ ├── pos-ssh-load-keys # Load SSH keys into ssh-agent\n46: │ ├── pos-vbox # Disposable Docker-based \"VMs\"\n47: │ ├── autostart.sh # Boot-time script (via systemd)\n48: │ ├── wr-* # Legacy wrappers → pos (backward compat)\n49: │ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos\n50: │ └── ssh-load-all # Legacy wrapper → pos ssh load-keys\n51: │\n52: ├── apps/ # Optional desktop app installers (by category)\n53: │ ├── install.sh # Interactive picker / orchestrator\n54: │ ├── browsers/\n55: │ │ └── brave.sh # Brave Browser (APT repo)\n56: │ ├── development/\n57: │ │ ├── opencode.sh # opencode AI agent (official script)\n58: │ │ └── vscode.sh # VS Code (Microsoft APT repo)\n59: │ ├── media/\n60: │ │ ├── obs.sh # OBS Studio (apt)\n61: │ │ ├── scrcpy.sh # scrcpy Android mirror (GitHub release)\n62: │ │ └── vlc.sh # VLC media player (apt)\n63: │ ├── networking/\n64: │ │ ├── netbird.sh # NetBird VPN (official script)\n65: │ │ ├── tailscale.sh # Tailscale VPN (official script)\n66: │ │ └── zerotier.sh # ZeroTier VPN (official script)\n67: │ ├── remote-access/\n68: │ │ ├── termius.sh # Termius SSH client (.deb)\n69: │ │ └── vnc-viewer.sh # TigerVNC Viewer (apt)\n70: │ ├── system/\n71: │ │ ├── docker.sh # Docker Engine (get.docker.com)\n72: │ │ └── qemu.sh # QEMU + libvirt + KVM (apt)\n73: │ └── utilities/\n74: │ ├── affine.sh # AFFiNE knowledge base (AppImage)\n75: │ ├── btop.sh # btop resource monitor (apt)\n76: │ └── localsend.sh # LocalSend (flatpak)\n77: │\n78: ├── completions/\n79: │ └── pos.bash # Bash tab-completion for the pos CLI\n80: │\n81: ├── compose/\n82: │ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n83: │\n84: ├── systemd/\n85: │ ├── autostart.service # Runs autostart.sh on boot\n86: │ └── ssh-agent.service # System-wide SSH agent socket\n87: │\n88: ├── README.md # User-facing documentation\n89: ├── DEV.md # Developer guide\n90: ├── .gitignore # Excludes secrets, Python artifacts, OS files\n91: └── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n92: ```\n93: \n94: ---\n95: \n96: ## 3. Installation Flow\n97: \n98: ```\n99: User runs: ./install.sh [--apps|--full|--dry-run|--skip |--steps ]\n100: │\n101: ├─ Phase 1: preinstall.sh (requires root)\n102: │ └─ apt update + installs 25+ packages + yt-dlp + fail2ban\n103: │\n104: ├─ Phase 2: install.sh (requires root)\n105: │ └─ Copies bin/* → /usr/local/bin/ (chmod 755)\n106: │ └─ Copies lib/common.sh → /usr/local/bin/common.sh (chmod 644)\n107: │\n108: ├─ Phase 3: postinstall.sh (runs as user)\n109: │ └─ Configures fail2ban (SSH jail: 5 retries, 1h ban)\n110: │ └─ PATH export in ~/.bashrc\n111: │ └─ Bash completion for pos CLI\n112: │ └─ Copies systemd/*.service → /etc/systemd/system/, enables them\n113: │\n114: ├─ Phase 4: ScaleTail clone\n115: │ └─ Shallow-clones ScaleTail templates to /usr/local/share/linux_post_install/scale-tail\n116: │\n117: └─ [if --apps or --full]: apps/install.sh\n118: └─ Interactive picker (or --all for non-interactive)\n119: ```\n120: \n121: **After install, the repo can be deleted** — all tools live in `/usr/local/bin/` and templates in `/usr/local/share/linux_post_install/`.\n122: \n123: ### install.sh Flags\n124: \n125: | Flag | Purpose |\n126: |------|---------|\n127: | `--apps` | Run interactive app picker after core install |\n128: | `--full` | Core install + all apps (non-interactive) |\n129: | `--dry-run` | Preview without executing |\n130: | `--skip ` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |\n131: | `--steps ` | Run only specific phases. Format: `1,3,4` or `1-3` |\n132: | `--no-color` | Disable colored output |\n133: \n134: ### pos Output Logging\n135: \n136: All non-interactive `pos` commands log output to `~/.local/share/linux_post_install/logs/`:\n137: - Per-command files: `YYYYMMDD_HHMMSS_pos_.log` (full stdout+stderr)\n138: - Main log: `pos.log` (command + timestamp + exit code for every invocation)\n139: - Interactive commands (`system-firewall`, `media-mp4`) only log invocation, not output\n140: \n141: ---\n142: \n143: ## 4. The `pos` CLI System\n144: \n145: ### How It Works\n146: \n147: `bin/pos` is the main dispatcher. It:\n148: 1. Scans its own directory for all executable `pos-*` files\n149: 2. Extracts category-subcommand names from filenames\n150: 3. Uses variable-length argument matching to find the right script\n151: \n152: **Example:** `pos docker compose up jellyfin`\n153: - Tries `pos-docker-compose-up-jellyfin` (not found)\n154: - Tries `pos-docker-compose-up` (not found)\n155: - Finds `pos-docker-compose` (runs with args `up jellyfin`)\n156: \n157: ### Available Commands\n158: \n159: | Category | Command | Script | Description |\n160: |----------|---------|--------|-------------|\n161: | network | ip | `pos-network-ip` | Show interfaces, routes, public IP |\n162: | network | checkport | `pos-network-checkport` | Check TCP port connectivity |\n163: | network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |\n164: | docker | ps | `pos-docker-ps` | Enhanced container overview |\n165: | docker | health | `pos-docker-health` | Quick health dashboard (exits 1 if unhealthy) |\n166: | docker | compose | `pos-docker-compose` | Service manager (ls/up/down/restart/logs/update/config) |\n167: | media | mp3 | `pos-media-mp3` | Download audio as MP3 |\n168: | media | mp4 | `pos-media-mp4` | Download video with format select |\n169: | system | firewall | `pos-system-firewall` | Interactive UFW management |\n170: | ssh | load-keys | `pos-ssh-load-keys` | Load SSH keys into agent |\n171: | vbox | create | `pos-vbox create` | Create disposable VM (asks \"Enter now?\") |\n172: | vbox | enter | `pos-vbox enter` | Start and exec into container |\n173: | vbox | ls | `pos-vbox ls` | List vbox-managed containers only (label-filtered) |\n174: | vbox | start/stop/rm | `pos-vbox start/stop/rm` | Lifecycle management |\n175: \n176: ### Legacy Wrappers\n177: \n178: These forward to `pos` transparently: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.\n179: \n180: ### pos vbox Details\n181: \n182: `pos-vbox` manages disposable Docker containers as lightweight VMs:\n183: \n184: - **Container labeling:** All created containers get `linux_post_install.vbox=true` label\n185: - **`ls` filtering:** `docker ps --filter label=linux_post_install.vbox=true` — only shows vbox-managed containers\n186: - **Post-create prompt:** After `create`, asks \"Enter now? [Y/n]\" using `confirm` helper\n187: - **Working dir detection:** `enter` auto-detects bind mount path from container labels\n188: - **Custom dirs:** `--dir ` or `--dir .` for current directory\n189: \n190: ---\n191: \n192: ## 5. Shared Library — `lib/common.sh`\n193: \n194: Sourced by most scripts. Provides:\n195: \n196: | Function | Purpose |\n197: |----------|---------|\n198: | `log \"msg\"` | Green `[+]` status message |\n199: | `warn \"msg\"` | Yellow `[!]` warning |\n200: | `err \"msg\"` | Red `ERROR:` + exit 1 |\n201: | `ok \"msg\"` | Green `OK` prefix |\n202: | `section \"title\"` | Cyan-bordered section header |\n203: | `step N T \"msg\"` | Numbered step header (e.g., `[1/4] Installing`) |\n204: | `run cmd` | Executes command, respects `$DRY_RUN` |\n205: | `spawn \"msg\" cmd` | Runs with animated braille spinner, elapsed time, OK/FAIL status |\n206: | `timer_start` / `timer_stop` | Elapsed time tracking |\n207: | `confirm \"prompt\" [default]` | y/N or Y/n prompt |\n208: \n209: **Auto-detects TTY** — disables colors when piped.\n210: \n211: **Source pattern:**\n212: ```bash\n213: source \"$(dirname \"$0\")/../lib/common.sh\"\n214: ```\n215: \n216: **Scripts that do NOT source common.sh** (self-contained): `bin/pos`, `pos-network-ip`, `pos-network-checkport`, `pos-network-scan`, `pos-media-mp3`, `pos-media-mp4`, `pos-ssh-load-keys`, `pos-system-firewall`.\n217: \n218: ---\n219: \n220: ## 6. Docker Compose / ScaleTail\n221: \n222: ### Architecture\n223: \n224: ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar pattern (`network_mode: service:tailscale`). Each service gets a `tail-xxxxx.ts.net` URL with optional automatic HTTPS.\n225: \n226: ```\n227: /usr/local/share/linux_post_install/scale-tail/ # Templates (git repo)\n228: └── services//\n229: ├── compose.yaml\n230: └── .env\n231: \n232: ~/.config/linux_post_install/compose.env # Global defaults (TS_AUTHKEY, TZ, DNS_SERVER, SERVICES_BASE)\n233: \n234: /srv// # Active deployments (default base)\n235: ├── compose.yaml # From template (refreshed on update)\n236: ├── .env # User config (preserved across updates)\n237: ├── config/\n238: └── data/\n239: ```\n240: \n241: ### Key Commands\n242: \n243: | Command | Description |\n244: |---------|-------------|\n245: | `pos docker compose ls` | List all available ScaleTail services |\n246: | `pos docker compose up ` | Deploy service to SERVICES_BASE |\n247: | `pos docker compose down ` | Stop a deployed service |\n248: | `pos docker compose restart ` | Restart a service |\n249: | `pos docker compose logs [-f]` | View/follow logs |\n250: | `pos docker compose update` | Pull latest templates, refresh compose.yaml (preserves .env) |\n251: | `pos docker compose config set K=V` | Set global config value |\n252: | `pos docker compose config show` | Display current config |\n253: \n254: ### Global Config Keys\n255: \n256: - `TS_AUTHKEY` — Tailscale auth key (required)\n257: - `TZ` — Timezone\n258: - `DNS_SERVER` — Custom DNS\n259: - `SERVICES_BASE` — Deployment root (default: `/srv`)\n260: \n261: ---\n262: \n263: ## 7. Optional Apps (`apps/`)\n264: \n265: ### How They Work\n266: \n267: - `apps/install.sh` auto-discovers all `apps//*.sh` files (excluding itself)\n268: - Three modes: interactive (default), `--all`, or specific app names as arguments\n269: - Interactive TUI groups apps by category with section headers\n270: - Each app script is standalone, idempotent, sources `lib/common.sh`\n271: \n272: ### Installation Methods\n273: \n274: | Method | Apps |\n275: |--------|------|\n276: | `apt install` | btop, obs, vlc, vnc-viewer, qemu |\n277: | APT repo (GPG + repo) | brave, vscode |\n278: | Official `curl \\| sh` | docker, tailscale, netbird, zerotier, opencode |\n279: | AppImage | affine |\n280: | GitHub release binary | scrcpy |\n281: | Flatpak | localsend |\n282: | .deb package | termius |\n283: \n284: ### Adding a New App\n285: \n286: 1. Create `apps/.sh` following the template in DEV.md\n287: 2. It auto-appears in the interactive picker — no registration needed\n288: \n289: ---\n290: \n291: ## 8. Systemd Services\n292: \n293: | Service | File | Purpose |\n294: |---------|------|---------|\n295: | `ssh-agent.service` | `systemd/ssh-agent.service` | System-wide SSH agent, socket at `/run/ssh-agent/socket` |\n296: | `autostart.service` | `systemd/autostart.service` | Runs `autostart.sh` on boot |\n297: \n298: All `.service` files in `systemd/` are automatically copied to `/etc/systemd/system/` and enabled by `postinstall.sh`.\n299: \n300: ---\n301: \n302: ## 9. Configuration Files\n303: \n304: ### Gitignored Secrets\n305: \n306: - `config/rclone.conf` — rclone remote config (OAuth tokens)\n307: - `config/authorized_keys` — SSH public keys\n308: \n309: ### Runtime Config\n310: \n311: - `~/.config/linux_post_install/compose.env` — Docker Compose global defaults\n312: - `~/.bashrc` — Modified by postinstall (PATH, bash completion)\n313: \n314: ---\n315: \n316: ## 10. Coding Conventions\n317: \n318: ### Script Standards\n319: \n320: - **Shebang:** `#!/usr/bin/env bash`\n321: - **Strict mode:** `set -euo pipefail`\n322: - **Help:** Every script accepts `-h`/`--help` via `case` pattern\n323: - **Idempotency:** Check existence before creating/modifying\n324: - **Exit codes:** 0 = success, 1 = error\n325: \n326: ### Naming Conventions\n327: \n328: - `pos--` — canonical tool names\n329: - `wr-*` — legacy wrappers\n330: - `apps//.sh` — optional app installers\n331: - Hyphens for word separation, lowercase always\n332: \n333: ### Error Handling\n334: \n335: - `command -v &>/dev/null` to check tool availability\n336: - `set -euo pipefail` for fail-fast\n337: - `err()` for fatal errors, `warn()` for non-fatal\n338: - Confirmation prompts for destructive actions\n339: \n340: ### Security\n341: \n342: - Never hardcode secrets in scripts\n343: - Use `chmod 600` for sensitive files\n344: - Validate user input before shell commands\n345: - Use `sudo` only where necessary\n346: \n347: ---\n348: \n349: ## 11. Development Workflow\n350: \n351: ### Adding a New App\n352: \n353: 1. Create `apps//.sh` following the template in DEV.md\n354: 2. It auto-appears in the interactive picker — no registration needed\n355: \n356: ### Adding a New Tool\n357: \n358: 1. Create `bin/pos--` following conventions\n359: 2. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)\n360: 3. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets)\n361: 4. Update `README.md`\n362: 5. Test: `bash -n bin/your-tool && shellcheck bin/your-tool`\n363: \n364: ### Testing\n365: \n366: ```bash\n367: # Syntax check all scripts\n368: for f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do\n369: bash -n \"$f\" || echo \"FAIL: $f\"\n370: done\n371: \n372: # ShellCheck linting\n373: shellcheck bin/my-script\n374: \n375: # Test in Docker\n376: docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash\n377: # inside: cd /repo && ./install.sh\n378: \n379: # Test apps interactively\n380: ./apps/install.sh --all\n381: ./apps/install.sh docker vscode\n382: ```\n383: \n384: ### Commit Conventions\n385: \n386: Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`\n387: \n388: ---\n389: \n390: ## 12. Key File Quick Reference\n391: \n392: | File | Lines | Purpose |\n393: |------|-------|---------|\n394: | `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n395: | `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n396: | `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n397: | `lib/common.sh` | 121 | Shared library |\n398: | `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n399: | `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n400: | `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n401: | `bin/pos-docker-ps` | 127 | Enhanced container overview |\n402: | `bin/pos-docker-health` | ~90 | Quick health dashboard |\n403: | `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n404: | `completions/pos.bash` | 118 | Dynamic bash completion |\n405: | `apps/install.sh` | 99 | App picker/orchestrator |\n406: \n407: ---\n408: \n409: ## 13. Common Tasks for Agents\n410: \n411: | Task | Where to Edit |\n412: |------|---------------|\n413: | Add a new CLI tool | Create `bin/pos--`, add deps in `preinstall.sh` |\n414: | Add a new app installer | Create `apps/.sh` (auto-discovered) |\n415: | Add a systemd service | Create `systemd/.service` (auto-installed by postinstall) |\n416: | Modify package list | Edit `PACKAGES` array in `preinstall.sh` |\n417: | Change PATH or bash config | Edit `postinstall.sh` |\n418: | Modify fail2ban config | Edit jail.local section in `postinstall.sh` |\n419: | Add bash completion | Edit `completions/pos.bash` |\n420: | Modify Docker Compose logic | Edit `bin/pos-docker-compose` |\n421: | Modify Docker health check | Edit `bin/pos-docker-health` |\n422: | Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |\n423: | Modify pos logging | Edit log setup in `bin/pos` |\n424: | Modify install phases/flags | Edit arg parsing in `install.sh` |\n425: | Update documentation | Edit `README.md` and/or `DEV.md` |\n426: | Add a secret config file | Add to `config/`, update `.gitignore`, add copy logic in `postinstall.sh` |\n\n(End of file - total 426 lines)\n", + "metadata": { + "preview": "# AGENT Context — Linux_post_install Project\n\n> **Purpose:** Single-source context document so any AI agent can understand the project, navigate the codebase, and make correct contributions.\n\n---\n\n## 1. Project Overview\n\n**Linux_post_install** is a personal bootstrap and homelab toolkit for Debian/Ubuntu. One command turns a bare install into a fully productive machine:\n\n- Automated system package installation (25+ packages)\n- A unified CLI (`pos`) for network, Docker, media, system, and SSH tasks\n- Optional desktop application installers (15 apps)\n- Docker Compose service management via ScaleTail templates (119+ self-hosted services with Tailscale sidecar)\n- Systemd service management for boot-time automation\n\n**Repository:** `https://gitea.skink-platy.ts.net/admin/Linux_post_install`\n**Target OS:** Debian / Ubuntu (uses `apt`)\n**Shell:** Bash (`#!/usr/bin/env bash`)\n", + "truncated": false, + "loaded": [], + "display": { + "type": "file", + "path": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "text": "# AGENT Context — Linux_post_install Project\n\n> **Purpose:** Single-source context document so any AI agent can understand the project, navigate the codebase, and make correct contributions.\n\n---\n\n## 1. Project Overview\n\n**Linux_post_install** is a personal bootstrap and homelab toolkit for Debian/Ubuntu. One command turns a bare install into a fully productive machine:\n\n- Automated system package installation (25+ packages)\n- A unified CLI (`pos`) for network, Docker, media, system, and SSH tasks\n- Optional desktop application installers (15 apps)\n- Docker Compose service management via ScaleTail templates (119+ self-hosted services with Tailscale sidecar)\n- Systemd service management for boot-time automation\n\n**Repository:** `https://gitea.skink-platy.ts.net/admin/Linux_post_install`\n**Target OS:** Debian / Ubuntu (uses `apt`)\n**Shell:** Bash (`#!/usr/bin/env bash`)\n\n---\n\n## 2. Directory Structure\n\n```\nLinux_post_install/\n├── install.sh # Main orchestrator — entry point\n├── preinstall.sh # Phase 1: system packages via apt + yt-dlp\n├── postinstall.sh # Phase 3: PATH, bash completion, systemd services\n│\n├── lib/\n│ └── common.sh # Shared library (colors, logging, spinner, timer, run)\n│\n├── bin/ # CLI tools — installed to /usr/local/bin/\n│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts\n│ ├── pos-network-ip # Show interfaces, routes, public IP\n│ ├── pos-network-checkport # TCP port checker\n│ ├── pos-network-scan # Parallel ping sweep of CIDR subnet\n│ ├── pos-docker-ps # Enhanced docker ps (health, IPs, ports, uptime)\n│ ├── pos-docker-health # Quick one-glance health dashboard\n│ ├── pos-docker-compose # Docker Compose service manager (largest script, 317 lines)\n│ ├── pos-media-mp3 # Audio downloader (yt-dlp → MP3)\n│ ├── pos-media-mp4 # Video downloader (yt-dlp → MP4, interactive format select)\n│ ├── pos-system-firewall # Interactive UFW manager (menu-driven, 284 lines)\n│ ├── pos-ssh-load-keys # Load SSH keys into ssh-agent\n│ ├── pos-vbox # Disposable Docker-based \"VMs\"\n│ ├── autostart.sh # Boot-time script (via systemd)\n│ ├── wr-* # Legacy wrappers → pos (backward compat)\n│ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos\n│ └── ssh-load-all # Legacy wrapper → pos ssh load-keys\n│\n├── apps/ # Optional desktop app installers (by category)\n│ ├── install.sh # Interactive picker / orchestrator\n│ ├── browsers/\n│ │ └── brave.sh # Brave Browser (APT repo)\n│ ├── development/\n│ │ ├── opencode.sh # opencode AI agent (official script)\n│ │ └── vscode.sh # VS Code (Microsoft APT repo)\n│ ├── media/\n│ │ ├── obs.sh # OBS Studio (apt)\n│ │ ├── scrcpy.sh # scrcpy Android mirror (GitHub release)\n│ │ └── vlc.sh # VLC media player (apt)\n│ ├── networking/\n│ │ ├── netbird.sh # NetBird VPN (official script)\n│ │ ├── tailscale.sh # Tailscale VPN (official script)\n│ │ └── zerotier.sh # ZeroTier VPN (official script)\n│ ├── remote-access/\n│ │ ├── termius.sh # Termius SSH client (.deb)\n│ │ └── vnc-viewer.sh # TigerVNC Viewer (apt)\n│ ├── system/\n│ │ ├── docker.sh # Docker Engine (get.docker.com)\n│ │ └── qemu.sh # QEMU + libvirt + KVM (apt)\n│ └── utilities/\n│ ├── affine.sh # AFFiNE knowledge base (AppImage)\n│ ├── btop.sh # btop resource monitor (apt)\n│ └── localsend.sh # LocalSend (flatpak)\n│\n├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── compose/\n│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n│\n├── systemd/\n│ ├── autostart.service # Runs autostart.sh on boot\n│ └── ssh-agent.service # System-wide SSH agent socket\n│\n├── README.md # User-facing documentation\n├── DEV.md # Developer guide\n├── .gitignore # Excludes secrets, Python artifacts, OS files\n└── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n```\n\n---\n\n## 3. Installation Flow\n\n```\nUser runs: ./install.sh [--apps|--full|--dry-run|--skip |--steps ]\n│\n├─ Phase 1: preinstall.sh (requires root)\n│ └─ apt update + installs 25+ packages + yt-dlp + fail2ban\n│\n├─ Phase 2: install.sh (requires root)\n│ └─ Copies bin/* → /usr/local/bin/ (chmod 755)\n│ └─ Copies lib/common.sh → /usr/local/bin/common.sh (chmod 644)\n│\n├─ Phase 3: postinstall.sh (runs as user)\n│ └─ Configures fail2ban (SSH jail: 5 retries, 1h ban)\n│ └─ PATH export in ~/.bashrc\n│ └─ Bash completion for pos CLI\n│ └─ Copies systemd/*.service → /etc/systemd/system/, enables them\n│\n├─ Phase 4: ScaleTail clone\n│ └─ Shallow-clones ScaleTail templates to /usr/local/share/linux_post_install/scale-tail\n│\n└─ [if --apps or --full]: apps/install.sh\n └─ Interactive picker (or --all for non-interactive)\n```\n\n**After install, the repo can be deleted** — all tools live in `/usr/local/bin/` and templates in `/usr/local/share/linux_post_install/`.\n\n### install.sh Flags\n\n| Flag | Purpose |\n|------|---------|\n| `--apps` | Run interactive app picker after core install |\n| `--full` | Core install + all apps (non-interactive) |\n| `--dry-run` | Preview without executing |\n| `--skip ` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |\n| `--steps ` | Run only specific phases. Format: `1,3,4` or `1-3` |\n| `--no-color` | Disable colored output |\n\n### pos Output Logging\n\nAll non-interactive `pos` commands log output to `~/.local/share/linux_post_install/logs/`:\n- Per-command files: `YYYYMMDD_HHMMSS_pos_.log` (full stdout+stderr)\n- Main log: `pos.log` (command + timestamp + exit code for every invocation)\n- Interactive commands (`system-firewall`, `media-mp4`) only log invocation, not output\n\n---\n\n## 4. The `pos` CLI System\n\n### How It Works\n\n`bin/pos` is the main dispatcher. It:\n1. Scans its own directory for all executable `pos-*` files\n2. Extracts category-subcommand names from filenames\n3. Uses variable-length argument matching to find the right script\n\n**Example:** `pos docker compose up jellyfin`\n- Tries `pos-docker-compose-up-jellyfin` (not found)\n- Tries `pos-docker-compose-up` (not found)\n- Finds `pos-docker-compose` (runs with args `up jellyfin`)\n\n### Available Commands\n\n| Category | Command | Script | Description |\n|----------|---------|--------|-------------|\n| network | ip | `pos-network-ip` | Show interfaces, routes, public IP |\n| network | checkport | `pos-network-checkport` | Check TCP port connectivity |\n| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |\n| docker | ps | `pos-docker-ps` | Enhanced container overview |\n| docker | health | `pos-docker-health` | Quick health dashboard (exits 1 if unhealthy) |\n| docker | compose | `pos-docker-compose` | Service manager (ls/up/down/restart/logs/update/config) |\n| media | mp3 | `pos-media-mp3` | Download audio as MP3 |\n| media | mp4 | `pos-media-mp4` | Download video with format select |\n| system | firewall | `pos-system-firewall` | Interactive UFW management |\n| ssh | load-keys | `pos-ssh-load-keys` | Load SSH keys into agent |\n| vbox | create | `pos-vbox create` | Create disposable VM (asks \"Enter now?\") |\n| vbox | enter | `pos-vbox enter` | Start and exec into container |\n| vbox | ls | `pos-vbox ls` | List vbox-managed containers only (label-filtered) |\n| vbox | start/stop/rm | `pos-vbox start/stop/rm` | Lifecycle management |\n\n### Legacy Wrappers\n\nThese forward to `pos` transparently: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.\n\n### pos vbox Details\n\n`pos-vbox` manages disposable Docker containers as lightweight VMs:\n\n- **Container labeling:** All created containers get `linux_post_install.vbox=true` label\n- **`ls` filtering:** `docker ps --filter label=linux_post_install.vbox=true` — only shows vbox-managed containers\n- **Post-create prompt:** After `create`, asks \"Enter now? [Y/n]\" using `confirm` helper\n- **Working dir detection:** `enter` auto-detects bind mount path from container labels\n- **Custom dirs:** `--dir ` or `--dir .` for current directory\n\n---\n\n## 5. Shared Library — `lib/common.sh`\n\nSourced by most scripts. Provides:\n\n| Function | Purpose |\n|----------|---------|\n| `log \"msg\"` | Green `[+]` status message |\n| `warn \"msg\"` | Yellow `[!]` warning |\n| `err \"msg\"` | Red `ERROR:` + exit 1 |\n| `ok \"msg\"` | Green `OK` prefix |\n| `section \"title\"` | Cyan-bordered section header |\n| `step N T \"msg\"` | Numbered step header (e.g., `[1/4] Installing`) |\n| `run cmd` | Executes command, respects `$DRY_RUN` |\n| `spawn \"msg\" cmd` | Runs with animated braille spinner, elapsed time, OK/FAIL status |\n| `timer_start` / `timer_stop` | Elapsed time tracking |\n| `confirm \"prompt\" [default]` | y/N or Y/n prompt |\n\n**Auto-detects TTY** — disables colors when piped.\n\n**Source pattern:**\n```bash\nsource \"$(dirname \"$0\")/../lib/common.sh\"\n```\n\n**Scripts that do NOT source common.sh** (self-contained): `bin/pos`, `pos-network-ip`, `pos-network-checkport`, `pos-network-scan`, `pos-media-mp3`, `pos-media-mp4`, `pos-ssh-load-keys`, `pos-system-firewall`.\n\n---\n\n## 6. Docker Compose / ScaleTail\n\n### Architecture\n\nScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar pattern (`network_mode: service:tailscale`). Each service gets a `tail-xxxxx.ts.net` URL with optional automatic HTTPS.\n\n```\n/usr/local/share/linux_post_install/scale-tail/ # Templates (git repo)\n└── services//\n ├── compose.yaml\n └── .env\n\n~/.config/linux_post_install/compose.env # Global defaults (TS_AUTHKEY, TZ, DNS_SERVER, SERVICES_BASE)\n\n/srv// # Active deployments (default base)\n ├── compose.yaml # From template (refreshed on update)\n ├── .env # User config (preserved across updates)\n ├── config/\n └── data/\n```\n\n### Key Commands\n\n| Command | Description |\n|---------|-------------|\n| `pos docker compose ls` | List all available ScaleTail services |\n| `pos docker compose up ` | Deploy service to SERVICES_BASE |\n| `pos docker compose down ` | Stop a deployed service |\n| `pos docker compose restart ` | Restart a service |\n| `pos docker compose logs [-f]` | View/follow logs |\n| `pos docker compose update` | Pull latest templates, refresh compose.yaml (preserves .env) |\n| `pos docker compose config set K=V` | Set global config value |\n| `pos docker compose config show` | Display current config |\n\n### Global Config Keys\n\n- `TS_AUTHKEY` — Tailscale auth key (required)\n- `TZ` — Timezone\n- `DNS_SERVER` — Custom DNS\n- `SERVICES_BASE` — Deployment root (default: `/srv`)\n\n---\n\n## 7. Optional Apps (`apps/`)\n\n### How They Work\n\n- `apps/install.sh` auto-discovers all `apps//*.sh` files (excluding itself)\n- Three modes: interactive (default), `--all`, or specific app names as arguments\n- Interactive TUI groups apps by category with section headers\n- Each app script is standalone, idempotent, sources `lib/common.sh`\n\n### Installation Methods\n\n| Method | Apps |\n|--------|------|\n| `apt install` | btop, obs, vlc, vnc-viewer, qemu |\n| APT repo (GPG + repo) | brave, vscode |\n| Official `curl \\| sh` | docker, tailscale, netbird, zerotier, opencode |\n| AppImage | affine |\n| GitHub release binary | scrcpy |\n| Flatpak | localsend |\n| .deb package | termius |\n\n### Adding a New App\n\n1. Create `apps/.sh` following the template in DEV.md\n2. It auto-appears in the interactive picker — no registration needed\n\n---\n\n## 8. Systemd Services\n\n| Service | File | Purpose |\n|---------|------|---------|\n| `ssh-agent.service` | `systemd/ssh-agent.service` | System-wide SSH agent, socket at `/run/ssh-agent/socket` |\n| `autostart.service` | `systemd/autostart.service` | Runs `autostart.sh` on boot |\n\nAll `.service` files in `systemd/` are automatically copied to `/etc/systemd/system/` and enabled by `postinstall.sh`.\n\n---\n\n## 9. Configuration Files\n\n### Gitignored Secrets\n\n- `config/rclone.conf` — rclone remote config (OAuth tokens)\n- `config/authorized_keys` — SSH public keys\n\n### Runtime Config\n\n- `~/.config/linux_post_install/compose.env` — Docker Compose global defaults\n- `~/.bashrc` — Modified by postinstall (PATH, bash completion)\n\n---\n\n## 10. Coding Conventions\n\n### Script Standards\n\n- **Shebang:** `#!/usr/bin/env bash`\n- **Strict mode:** `set -euo pipefail`\n- **Help:** Every script accepts `-h`/`--help` via `case` pattern\n- **Idempotency:** Check existence before creating/modifying\n- **Exit codes:** 0 = success, 1 = error\n\n### Naming Conventions\n\n- `pos--` — canonical tool names\n- `wr-*` — legacy wrappers\n- `apps//.sh` — optional app installers\n- Hyphens for word separation, lowercase always\n\n### Error Handling\n\n- `command -v &>/dev/null` to check tool availability\n- `set -euo pipefail` for fail-fast\n- `err()` for fatal errors, `warn()` for non-fatal\n- Confirmation prompts for destructive actions\n\n### Security\n\n- Never hardcode secrets in scripts\n- Use `chmod 600` for sensitive files\n- Validate user input before shell commands\n- Use `sudo` only where necessary\n\n---\n\n## 11. Development Workflow\n\n### Adding a New App\n\n1. Create `apps//.sh` following the template in DEV.md\n2. It auto-appears in the interactive picker — no registration needed\n\n### Adding a New Tool\n\n1. Create `bin/pos--` following conventions\n2. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)\n3. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets)\n4. Update `README.md`\n5. Test: `bash -n bin/your-tool && shellcheck bin/your-tool`\n\n### Testing\n\n```bash\n# Syntax check all scripts\nfor f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do\n bash -n \"$f\" || echo \"FAIL: $f\"\ndone\n\n# ShellCheck linting\nshellcheck bin/my-script\n\n# Test in Docker\ndocker run --rm -it -v $PWD:/repo ubuntu:22.04 bash\n# inside: cd /repo && ./install.sh\n\n# Test apps interactively\n./apps/install.sh --all\n./apps/install.sh docker vscode\n```\n\n### Commit Conventions\n\nUse conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`\n\n---\n\n## 12. Key File Quick Reference\n\n| File | Lines | Purpose |\n|------|-------|---------|\n| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n| `lib/common.sh` | 121 | Shared library |\n| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n| `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n| `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n| `bin/pos-docker-ps` | 127 | Enhanced container overview |\n| `bin/pos-docker-health` | ~90 | Quick health dashboard |\n| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n| `completions/pos.bash` | 118 | Dynamic bash completion |\n| `apps/install.sh` | 99 | App picker/orchestrator |\n\n---\n\n## 13. Common Tasks for Agents\n\n| Task | Where to Edit |\n|------|---------------|\n| Add a new CLI tool | Create `bin/pos--`, add deps in `preinstall.sh` |\n| Add a new app installer | Create `apps/.sh` (auto-discovered) |\n| Add a systemd service | Create `systemd/.service` (auto-installed by postinstall) |\n| Modify package list | Edit `PACKAGES` array in `preinstall.sh` |\n| Change PATH or bash config | Edit `postinstall.sh` |\n| Modify fail2ban config | Edit jail.local section in `postinstall.sh` |\n| Add bash completion | Edit `completions/pos.bash` |\n| Modify Docker Compose logic | Edit `bin/pos-docker-compose` |\n| Modify Docker health check | Edit `bin/pos-docker-health` |\n| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |\n| Modify pos logging | Edit log setup in `bin/pos` |\n| Modify install phases/flags | Edit arg parsing in `install.sh` |\n| Update documentation | Edit `README.md` and/or `DEV.md` |\n| Add a secret config file | Add to `config/`, update `.gitignore`, add copy logic in `postinstall.sh` |", + "lineStart": 1, + "lineEnd": 426, + "totalLines": 426, + "truncated": false + } + }, + "title": "AGENT_Context_Project.md", + "time": { + "start": 1785353912067, + "end": 1785353912475 + } + }, + "id": "prt_faf62beb4001HP20oez7WSKPsN", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + }, + { + "type": "tool", + "tool": "read", + "callID": "call_01_mJv23mUsCm61xgm4rXBA8334", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/README.md" + }, + "output": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/README.md\nfile\n\n1: # Linux_post_install — Personal Bootstrap & Homelab Toolkit\n2: \n3: > One command turns a bare Debian/Ubuntu install into a fully productive machine.\n4: \n5: ---\n6: \n7: ## What Is This\n8: \n9: After reinstalling Linux, you usually need to install packages, set up SSH, configure the firewall, and install apps. This repo automates all of that in one go.\n10: \n11: **What you get:**\n12: \n13: - **25+ system packages** installed automatically (git, curl, tmux, ufw, fail2ban, etc.)\n14: - **Unified `pos` CLI** — one command for network, Docker, media, system, and SSH tasks\n15: - **119+ self-hosted services** via ScaleTail + Tailscale (Jellyfin, Home Assistant, etc.)\n16: - **15 optional desktop apps** (VS Code, Docker Desktop, Brave, OBS, etc.) — pick what you want\n17: - **systemd services** for SSH agent and boot-time automation\n18: - **Everything in `/usr/local/bin/`** — you can delete the repo after install\n19: \n20: ---\n21: \n22: ## Quick Start\n23: \n24: ```bash\n25: git clone https://gitea.skink-platy.ts.net/admin/Linux_post_install.git\n26: cd Linux_post_install\n27: ./install.sh # core: packages + CLI + services + ScaleTail\n28: ./install.sh --apps # core + interactive app picker\n29: ./install.sh --full # core + all apps (non-interactive)\n30: ```\n31: \n32: **Flags:**\n33: \n34: | Flag | What it does |\n35: |------|-------------|\n36: | `--apps` | Run interactive app picker after core install |\n37: | `--full` | Core install + all apps (no prompts) |\n38: | `--dry-run` | Preview without executing anything |\n39: | `--skip ` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint` |\n40: | `--steps ` | Run specific phases only, e.g. `--steps 1,3` or `--steps 1-3` |\n41: | `--no-color` | Disable colored output |\n42: \n43: ---\n44: \n45: ## What Gets Installed\n46: \n47: | Phase | Script | What happens |\n48: |-------|--------|-------------|\n49: | 1 | `preinstall.sh` | `apt update` + 25+ packages + yt-dlp + fail2ban |\n50: | 2 | `install.sh` | Copies all `bin/` tools to `/usr/local/bin/` |\n51: | 3 | `postinstall.sh` | Configures fail2ban, SSH agent, PATH, bash completion, systemd services |\n52: | 4 | ScaleTail clone | Downloads 119+ Docker Compose templates with Tailscale sidecar |\n53: | 5 (opt) | `apps/install.sh` | Installs desktop apps you select |\n54: \n55: ---\n56: \n57: ## The `pos` CLI\n58: \n59: After install, use the `pos` command for everything:\n60: \n61: ```bash\n62: pos # list available categories\n63: pos help network # help for a specific category\n64: ```\n65: \n66: ### Network\n67: \n68: ```bash\n69: pos network ip # Show interfaces, routes, public IP\n70: pos network checkport 192.168.1.1:80 # Check if a TCP port is open\n71: pos network scan 192.168.8.0/24 # Fast parallel ping sweep\n72: ```\n73: \n74: ### Docker\n75: \n76: ```bash\n77: pos docker ps # List containers with health, IPs, ports\n78: pos docker health # Health dashboard (exits 1 if unhealthy)\n79: ```\n80: \n81: #### Compose (ScaleTail — 119+ self-hosted services)\n82: \n83: Each service runs with a Tailscale sidecar and gets its own `tail-xxxxx.ts.net` URL.\n84: \n85: **Quick start:**\n86: \n87: ```bash\n88: # 1. Set your Tailscale auth key (required once)\n89: pos docker compose config set TS_AUTHKEY=tskey-auth-xxxxx\n90: \n91: # 2. Deploy a service\n92: pos docker compose up jellyfin\n93: \n94: # 3. Open https://jellyfin.tail-xxxxx.ts.net\n95: ```\n96: \n97: **All commands:**\n98: \n99: ```bash\n100: pos docker compose ls # List available service templates\n101: pos docker compose installed # List deployed services\n102: pos docker compose up jellyfin # Deploy or start a service\n103: pos docker compose down actual-budget # Stop a service\n104: pos docker compose logs home-assistant -f # Tail logs\n105: pos docker compose restart home-assistant # Restart a service\n106: pos docker compose update # Pull latest templates + refresh deployed compose files\n107: pos docker compose config # Show current configuration\n108: pos docker compose config set TZ=Asia/Tokyo # Set a global default\n109: pos docker compose config edit # Open config in editor\n110: ```\n111: \n112: **Config strategy — three layers:**\n113: \n114: | Layer | File | Purpose |\n115: |-------|------|---------|\n116: | Template defaults | `/usr/local/share/linux_post_install/scale-tail/services//.env` | Per-service defaults from ScaleTail |\n117: | Global config | `~/.config/linux_post_install/compose.env` | Your defaults — applies to all services |\n118: | Per-service | `/srv//.env` | Actual config — created on first deploy, **never overwritten** |\n119: \n120: Set global defaults once, then every `up` fills them into the new service's `.env`.\n121: \n122: **Paths:**\n123: \n124: - Templates: `/usr/local/share/linux_post_install/scale-tail/services/`\n125: - Deployments: `/srv//` (configurable via `SERVICES_BASE`)\n126: - Global config: `~/.config/linux_post_install/compose.env`\n127: \n128: **Config keys:**\n129: \n130: | Key | What it does |\n131: |-----|-------------|\n132: | `TS_AUTHKEY` | Tailscale auth key (required for sidecar networking) |\n133: | `TZ` | Timezone for the service |\n134: | `DNS_SERVER` | Custom DNS server |\n135: | `SERVICES_BASE` | Where services are deployed (default: `/srv`)\n136: \n137: ### VBox (disposable Docker containers)\n138: \n139: ```bash\n140: pos vbox create lab1 # Create — prompts to enter\n141: pos vbox create lab1 --dir . # Bind mount current directory\n142: pos vbox enter lab1 # Auto-starts if stopped\n143: pos vbox ls # List vbox containers only\n144: pos vbox stop/start/rm lab1\n145: ```\n146: \n147: ### Media\n148: \n149: ```bash\n150: pos media mp3 https://youtube.com/watch?v=... # Audio → MP3\n151: pos media mp4 https://youtube.com/watch?v=... # Video with format selection\n152: ```\n153: \n154: ### System\n155: \n156: ```bash\n157: sudo pos system firewall # Interactive UFW manager\n158: ```\n159: \n160: ### SSH\n161: \n162: ```bash\n163: pos ssh load-keys # Load all SSH keys into agent\n164: ```\n165: \n166: The `ssh-agent.service` runs at boot. `SSH_AUTH_SOCK` is set in `~/.bashrc`.\n167: \n168: ### Legacy wrappers\n169: \n170: These still work and forward to `pos`: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.\n171: \n172: ---\n173: \n174: ## Optional Apps\n175: \n176: Install with `./apps/install.sh` (interactive), `./apps/install.sh --all`, or by name:\n177: \n178: | Category | Apps |\n179: |----------|------|\n180: | Browsers | Brave |\n181: | Development | opencode, VS Code |\n182: | Media | OBS Studio, scrcpy, VLC |\n183: | Networking | NetBird, Tailscale, ZeroTier |\n184: | Remote Access | Termius, VNC Viewer |\n185: | System | Docker Engine, QEMU + KVM |\n186: | Utilities | AFFiNE, btop, LocalSend |\n187: \n188: ---\n189: \n190: ## Development\n191: \n192: See [DEV.md](DEV.md) for architecture, conventions, and how to add or modify tools.\n\n(End of file - total 192 lines)\n", + "metadata": { + "preview": "# Linux_post_install — Personal Bootstrap & Homelab Toolkit\n\n> One command turns a bare Debian/Ubuntu install into a fully productive machine.\n\n---\n\n## What Is This\n\nAfter reinstalling Linux, you usually need to install packages, set up SSH, configure the firewall, and install apps. This repo automates all of that in one go.\n\n**What you get:**\n\n- **25+ system packages** installed automatically (git, curl, tmux, ufw, fail2ban, etc.)\n- **Unified `pos` CLI** — one command for network, Docker, media, system, and SSH tasks\n- **119+ self-hosted services** via ScaleTail + Tailscale (Jellyfin, Home Assistant, etc.)\n- **15 optional desktop apps** (VS Code, Docker Desktop, Brave, OBS, etc.) — pick what you want\n- **systemd services** for SSH agent and boot-time automation\n- **Everything in `/usr/local/bin/`** — you can delete the repo after install\n\n---", + "truncated": false, + "loaded": [], + "display": { + "type": "file", + "path": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/README.md", + "text": "# Linux_post_install — Personal Bootstrap & Homelab Toolkit\n\n> One command turns a bare Debian/Ubuntu install into a fully productive machine.\n\n---\n\n## What Is This\n\nAfter reinstalling Linux, you usually need to install packages, set up SSH, configure the firewall, and install apps. This repo automates all of that in one go.\n\n**What you get:**\n\n- **25+ system packages** installed automatically (git, curl, tmux, ufw, fail2ban, etc.)\n- **Unified `pos` CLI** — one command for network, Docker, media, system, and SSH tasks\n- **119+ self-hosted services** via ScaleTail + Tailscale (Jellyfin, Home Assistant, etc.)\n- **15 optional desktop apps** (VS Code, Docker Desktop, Brave, OBS, etc.) — pick what you want\n- **systemd services** for SSH agent and boot-time automation\n- **Everything in `/usr/local/bin/`** — you can delete the repo after install\n\n---\n\n## Quick Start\n\n```bash\ngit clone https://gitea.skink-platy.ts.net/admin/Linux_post_install.git\ncd Linux_post_install\n./install.sh # core: packages + CLI + services + ScaleTail\n./install.sh --apps # core + interactive app picker\n./install.sh --full # core + all apps (non-interactive)\n```\n\n**Flags:**\n\n| Flag | What it does |\n|------|-------------|\n| `--apps` | Run interactive app picker after core install |\n| `--full` | Core install + all apps (no prompts) |\n| `--dry-run` | Preview without executing anything |\n| `--skip ` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint` |\n| `--steps ` | Run specific phases only, e.g. `--steps 1,3` or `--steps 1-3` |\n| `--no-color` | Disable colored output |\n\n---\n\n## What Gets Installed\n\n| Phase | Script | What happens |\n|-------|--------|-------------|\n| 1 | `preinstall.sh` | `apt update` + 25+ packages + yt-dlp + fail2ban |\n| 2 | `install.sh` | Copies all `bin/` tools to `/usr/local/bin/` |\n| 3 | `postinstall.sh` | Configures fail2ban, SSH agent, PATH, bash completion, systemd services |\n| 4 | ScaleTail clone | Downloads 119+ Docker Compose templates with Tailscale sidecar |\n| 5 (opt) | `apps/install.sh` | Installs desktop apps you select |\n\n---\n\n## The `pos` CLI\n\nAfter install, use the `pos` command for everything:\n\n```bash\npos # list available categories\npos help network # help for a specific category\n```\n\n### Network\n\n```bash\npos network ip # Show interfaces, routes, public IP\npos network checkport 192.168.1.1:80 # Check if a TCP port is open\npos network scan 192.168.8.0/24 # Fast parallel ping sweep\n```\n\n### Docker\n\n```bash\npos docker ps # List containers with health, IPs, ports\npos docker health # Health dashboard (exits 1 if unhealthy)\n```\n\n#### Compose (ScaleTail — 119+ self-hosted services)\n\nEach service runs with a Tailscale sidecar and gets its own `tail-xxxxx.ts.net` URL.\n\n**Quick start:**\n\n```bash\n# 1. Set your Tailscale auth key (required once)\npos docker compose config set TS_AUTHKEY=tskey-auth-xxxxx\n\n# 2. Deploy a service\npos docker compose up jellyfin\n\n# 3. Open https://jellyfin.tail-xxxxx.ts.net\n```\n\n**All commands:**\n\n```bash\npos docker compose ls # List available service templates\npos docker compose installed # List deployed services\npos docker compose up jellyfin # Deploy or start a service\npos docker compose down actual-budget # Stop a service\npos docker compose logs home-assistant -f # Tail logs\npos docker compose restart home-assistant # Restart a service\npos docker compose update # Pull latest templates + refresh deployed compose files\npos docker compose config # Show current configuration\npos docker compose config set TZ=Asia/Tokyo # Set a global default\npos docker compose config edit # Open config in editor\n```\n\n**Config strategy — three layers:**\n\n| Layer | File | Purpose |\n|-------|------|---------|\n| Template defaults | `/usr/local/share/linux_post_install/scale-tail/services//.env` | Per-service defaults from ScaleTail |\n| Global config | `~/.config/linux_post_install/compose.env` | Your defaults — applies to all services |\n| Per-service | `/srv//.env` | Actual config — created on first deploy, **never overwritten** |\n\nSet global defaults once, then every `up` fills them into the new service's `.env`.\n\n**Paths:**\n\n- Templates: `/usr/local/share/linux_post_install/scale-tail/services/`\n- Deployments: `/srv//` (configurable via `SERVICES_BASE`)\n- Global config: `~/.config/linux_post_install/compose.env`\n\n**Config keys:**\n\n| Key | What it does |\n|-----|-------------|\n| `TS_AUTHKEY` | Tailscale auth key (required for sidecar networking) |\n| `TZ` | Timezone for the service |\n| `DNS_SERVER` | Custom DNS server |\n| `SERVICES_BASE` | Where services are deployed (default: `/srv`)\n\n### VBox (disposable Docker containers)\n\n```bash\npos vbox create lab1 # Create — prompts to enter\npos vbox create lab1 --dir . # Bind mount current directory\npos vbox enter lab1 # Auto-starts if stopped\npos vbox ls # List vbox containers only\npos vbox stop/start/rm lab1\n```\n\n### Media\n\n```bash\npos media mp3 https://youtube.com/watch?v=... # Audio → MP3\npos media mp4 https://youtube.com/watch?v=... # Video with format selection\n```\n\n### System\n\n```bash\nsudo pos system firewall # Interactive UFW manager\n```\n\n### SSH\n\n```bash\npos ssh load-keys # Load all SSH keys into agent\n```\n\nThe `ssh-agent.service` runs at boot. `SSH_AUTH_SOCK` is set in `~/.bashrc`.\n\n### Legacy wrappers\n\nThese still work and forward to `pos`: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.\n\n---\n\n## Optional Apps\n\nInstall with `./apps/install.sh` (interactive), `./apps/install.sh --all`, or by name:\n\n| Category | Apps |\n|----------|------|\n| Browsers | Brave |\n| Development | opencode, VS Code |\n| Media | OBS Studio, scrcpy, VLC |\n| Networking | NetBird, Tailscale, ZeroTier |\n| Remote Access | Termius, VNC Viewer |\n| System | Docker Engine, QEMU + KVM |\n| Utilities | AFFiNE, btop, LocalSend |\n\n---\n\n## Development\n\nSee [DEV.md](DEV.md) for architecture, conventions, and how to add or modify tools.", + "lineStart": 1, + "lineEnd": 192, + "totalLines": 192, + "truncated": false + } + }, + "title": "README.md", + "time": { + "start": 1785353912283, + "end": 1785353912556 + } + }, + "id": "prt_faf62bf56001KWiMRYEEEs6yBw", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + }, + { + "type": "tool", + "tool": "read", + "callID": "call_02_6dA4gFaCG7feRqMM7ney0283", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/DEV.md" + }, + "output": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/DEV.md\nfile\n\n1: # Development Guide\n2: \n3: How this repo works, how to add features, and what to keep in mind when editing.\n4: \n5: ---\n6: \n7: ## Architecture\n8: \n9: ### Installation Phases\n10: \n11: ```\n12: install.sh\n13: │\n14: ┌───────────┼───────────┐\n15: ▼ ▼ ▼\n16: preinstall.sh bin/* postinstall.sh\n17: (packages) → /usr/local/bin (config + services)\n18: ```\n19: \n20: | Phase | Script | Responsibility |\n21: |-------|--------|----------------|\n22: | Pre | `preinstall.sh` | System packages, apt repos, global binaries (yt-dlp) |\n23: | Install | `install.sh` | Copies `bin/*` → `/usr/local/bin/` (chmod 755), `lib/common.sh` → `/usr/local/bin/common.sh` |\n24: | Post | `postinstall.sh` | User config (SSH keys, PATH, bash completion), systemd services |\n25: \n26: Each phase is independent and runs only if the corresponding script exists.\n27: \n28: ### Directory Layout\n29: \n30: | Directory | Purpose | Installed To |\n31: |-----------|---------|-------------|\n32: | `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |\n33: | `apps//` | Optional desktop app installers | run on demand |\n34: | `lib/` | Shared library (`common.sh`) | sourced at build time |\n35: | `config/` | Gitignored user config files | `~/.config//` (via postinstall) |\n36: | `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` |\n37: | `systemd/` | Systemd unit files | `/etc/systemd/system/` (via postinstall) |\n38: \n39: ### The `pos` CLI\n40: \n41: `bin/pos` is a smart dispatcher. It scans its own directory for executable `pos-*` files and uses variable-length argument matching:\n42: \n43: ```\n44: pos docker compose up jellyfin\n45: → tries pos-docker-compose-up-jellyfin (not found)\n46: → tries pos-docker-compose-up (not found)\n47: → finds pos-docker-compose → runs with args \"up jellyfin\"\n48: ```\n49: \n50: All non-interactive commands log to `~/.local/share/linux_post_install/logs/`.\n51: \n52: ### Shared Library (`lib/common.sh`)\n53: \n54: Sourced by most scripts. Key functions:\n55: \n56: | Function | Purpose |\n57: |----------|---------|\n58: | `log \"msg\"` | Green `[+]` status message |\n59: | `warn \"msg\"` | Yellow `[!]` warning |\n60: | `err \"msg\"` | Red `ERROR:` + exit 1 |\n61: | `ok \"msg\"` | Green `OK` prefix |\n62: | `section \"title\"` | Cyan-bordered section header |\n63: | `step N T \"msg\"` | Numbered step header |\n64: | `run cmd` | Executes command, respects `$DRY_RUN` |\n65: | `spawn \"msg\" cmd` | Animated braille spinner + elapsed time |\n66: | `timer_start` / `timer_stop` | Elapsed time tracking |\n67: | `confirm \"prompt\"` | y/N prompt with optional default |\n68: \n69: ---\n70: \n71: ## Adding a New CLI Tool\n72: \n73: ### 1. Create the script\n74: \n75: ```bash\n76: #!/usr/bin/env bash\n77: \n78: set -euo pipefail\n79: \n80: source \"$(dirname \"$0\")/../lib/common.sh\"\n81: \n82: usage() {\n83: cat <\n85: EOF\n86: exit 0\n87: }\n88: \n89: case \"${1:-}\" in\n90: -h|--help|\"\") usage ;;\n91: esac\n92: \n93: # --- script logic ---\n94: ```\n95: \n96: **Conventions:**\n97: - Shebang: `#!/usr/bin/env bash`\n98: - Strict mode: `set -euo pipefail`\n99: - `--help` flag: accept `-h` / `--help` via `case` pattern\n100: - Shared library: always source `common.sh` for colors, logging, spinners\n101: - Exit codes: `0` success, `1` error\n102: - No shared lib? Inline fallbacks:\n103: ```bash\n104: log() { echo \"[+] $*\"; }\n105: warn() { echo \"[!] $*\"; }\n106: err() { echo \"ERROR: $*\" >&2; exit 1; }\n107: ```\n108: \n109: ### 2. Add system dependencies\n110: \n111: Add package names to the `PACKAGES` array in `preinstall.sh`:\n112: \n113: ```bash\n114: PACKAGES=(\n115: ...\n116: your-package\n117: )\n118: ```\n119: \n120: ### 3. Add config files (if needed)\n121: \n122: Place defaults in `config/` and add copy logic to `postinstall.sh`. If they contain secrets, add to `.gitignore` and document in README.\n123: \n124: ### 4. Add SSH keys (if needed)\n125: \n126: Place public keys in `config/authorized_keys` (one per line). `postinstall.sh` reads this file automatically.\n127: \n128: ### 5. Update README\n129: \n130: Add a section under the relevant category in README.md.\n131: \n132: ### 6. Test\n133: \n134: ```bash\n135: bash -n bin/your-tool\n136: shellcheck bin/your-tool\n137: ./bin/your-tool --help\n138: ```\n139: \n140: ---\n141: \n142: ## Adding an Optional App\n143: \n144: ### 1. Create the installer\n145: \n146: ```bash\n147: #!/usr/bin/env bash\n148: set -euo pipefail\n149: source \"$(dirname \"$0\")/../../lib/common.sh\"\n150: \n151: install_myapp() {\n152: command -v myapp &>/dev/null && { log \"myapp already installed\"; return 0; }\n153: spawn \"Installing myapp\" sudo apt install -y myapp\n154: }\n155: \n156: install_myapp\n157: ```\n158: \n159: Place it in `apps//.sh`. It auto-appears in the picker — no registration needed.\n160: \n161: **Categories:** `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`\n162: \n163: ### 2. Conventions\n164: \n165: - Idempotent: check `command -v` before installing\n166: - APT packages → `sudo apt install -y` inside `spawn`\n167: - Official scripts → `curl ... | sh` inside `spawn`\n168: - Flatpak → `flatpak install -y flathub ` inside `spawn`\n169: - `.deb` files → download to temp, `sudo apt install -y ./file.deb` inside `spawn`\n170: - `usermod` for groups → print re-login reminder\n171: \n172: ---\n173: \n174: ## Editing an Existing Tool\n175: \n176: 1. Find the script in `bin/`\n177: 2. Understand its contract (args, output, exit codes)\n178: 3. Make the change — keep it idempotent\n179: 4. Update README if behaviour changed\n180: 5. Run `shellcheck` on the modified file\n181: \n182: ---\n183: \n184: ## Best Practices\n185: \n186: ### Idempotency\n187: \n188: Check before creating, use `>>` with grep guards, don't overwrite user configs.\n189: \n190: ### Error Handling\n191: \n192: ```bash\n193: set -euo pipefail\n194: command -v docker &>/dev/null || { echo \"docker not found\"; exit 1; }\n195: [[ -n \"${1:-}\" ]] || { echo \"Usage: my-tool \"; exit 1; }\n196: ```\n197: \n198: ### Portability\n199: \n200: Targets **Debian** and **Ubuntu**. Use `apt`, assume bash at `/usr/bin/env bash`, check tools with `command -v`.\n201: \n202: ### Dry-run Support\n203: \n204: Scripts support `--dry-run`. Use the `run()` helper:\n205: \n206: ```bash\n207: run() {\n208: if [ \"$DRY_RUN\" -eq 1 ]; then\n209: log \"(dry-run) $*\"\n210: else\n211: \"$@\"\n212: fi\n213: }\n214: run sudo apt install -y git\n215: ```\n216: \n217: ### Security\n218: \n219: - Never hardcode secrets — put them in `config/` (gitignored)\n220: - `chmod 600` for sensitive files\n221: - Validate input before shell commands\n222: - Use `sudo` only where needed\n223: \n224: ### Naming\n225: \n226: - CLI tools: `bin/pos--`\n227: - Legacy wrappers: `bin/wr-*`\n228: - App installers: `apps//.sh`\n229: - Lowercase with hyphens\n230: \n231: ---\n232: \n233: ## Working with Systemd\n234: \n235: Create `systemd/.service` — `postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically.\n236: \n237: ```ini\n238: [Unit]\n239: Description=My Service\n240: After=network-online.target\n241: Wants=network-online.target\n242: \n243: [Service]\n244: Type=simple\n245: ExecStart=/usr/local/bin/your-script.sh\n246: Restart=on-failure\n247: RestartSec=10\n248: \n249: [Install]\n250: WantedBy=multi-user.target\n251: ```\n252: \n253: ---\n254: \n255: ## Working with Config Files\n256: \n257: 1. Place the file in `config/`\n258: 2. Add copy logic to `postinstall.sh`:\n259: \n260: ```bash\n261: if [ -f config/your-config.conf ]; then\n262: mkdir -p \"$HOME/.config/your-app\"\n263: cp config/your-config.conf \"$HOME/.config/your-app/your-config.conf\"\n264: chmod 600 \"$HOME/.config/your-app/your-config.conf\"\n265: fi\n266: ```\n267: \n268: ---\n269: \n270: ## Docker Compose / ScaleTail\n271: \n272: 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. Each service gets a `tail-xxxxx.ts.net` URL via `network_mode: service:tailscale`.\n273: \n274: ### Config Strategy — Three Layers\n275: \n276: Values cascade from least to most specific:\n277: \n278: ```\n279: Template .env (per-service defaults from ScaleTail)\n280: ↓\n281: Global config (~/.config/linux_post_install/compose.env)\n282: ↓\n283: Per-service .env (/srv//.env) — created on first deploy, NEVER overwritten\n284: ```\n285: \n286: On first `pos docker compose up `:\n287: 1. Template `.env` is copied to `/srv//.env`\n288: 2. Matching keys from global config are filled in\n289: 3. If `TS_AUTHKEY` is still empty, you're prompted to enter it\n290: 4. After that, the per-service `.env` is **never touched** — not even by `update`\n291: \n292: ### Layout\n293: \n294: | Path | Purpose | Mutability |\n295: |------|---------|------------|\n296: | `/usr/local/share/linux_post_install/scale-tail/services//` | ScaleTail templates (git repo) | Read-only |\n297: | `~/.config/linux_post_install/compose.env` | Your global defaults | Edit via `config set` or `config edit` |\n298: | `/srv//` | Active deployment | Per-service `.env` preserved forever |\n299: \n300: ### Key Commands\n301: \n302: | Command | Behaviour |\n303: |---------|-----------|\n304: | `pos docker compose up ` | Deploys to `$SERVICES_BASE//`, creates `config/` + `data/`, generates `.env` from global defaults |\n305: | `pos docker compose down ` | Stops the stack |\n306: | `pos docker compose update` | `git pull` templates + refreshes `compose.yaml` for all deployed services (`.env` untouched) |\n307: | `pos docker compose config set K=V` | Sets a global default in `~/.config/linux_post_install/compose.env` |\n308: | `pos docker compose config show` | Displays current global config and `SERVICES_BASE` |\n309: | `pos docker compose config edit` | Opens global config in `$EDITOR` |\n310: \n311: ### Global Config Keys\n312: \n313: | Key | Required | Default | Purpose |\n314: |-----|----------|---------|---------|\n315: | `TS_AUTHKEY` | Yes | — | Tailscale auth key for sidecar networking |\n316: | `TZ` | No | `Europe/Amsterdam` | Timezone for services |\n317: | `DNS_SERVER` | No | `9.9.9.9` | Custom DNS server |\n318: | `SERVICES_BASE` | No | `/srv` | Root directory for all deployments |\n319: \n320: ---\n321: \n322: ## Commit Guidelines\n323: \n324: - Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`\n325: - Explain *why*, not just *what*\n326: - One logical change per commit\n327: \n328: ```\n329: feat: add pos-disk-usage for monitoring disk space\n330: fix: pos-network-ip fails when no default route exists\n331: docs: add example output for pos-network-scan\n332: ```\n333: \n334: ---\n335: \n336: ## Useful Commands\n337: \n338: ```bash\n339: # Syntax check a single script\n340: bash -n bin/my-script\n341: \n342: # ShellCheck linting\n343: shellcheck bin/my-script\n344: \n345: # Check all scripts\n346: for f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do\n347: bash -n \"$f\" || echo \"FAIL: $f\"\n348: done\n349: \n350: # Init submodule\n351: git submodule update --init\n352: \n353: # Pull latest ScaleTail templates\n354: git submodule update --remote compose/scale-tail\n355: \n356: # Test install in Docker\n357: docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash\n358: # inside: cd /repo && ./install.sh\n359: \n360: # Test app installers\n361: ./apps/install.sh\n362: ./apps/install.sh --all\n363: ./apps/install.sh docker vscode\n364: ```\n\n(End of file - total 364 lines)\n", + "metadata": { + "preview": "# Development Guide\n\nHow this repo works, how to add features, and what to keep in mind when editing.\n\n---\n\n## Architecture\n\n### Installation Phases\n\n```\n install.sh\n │\n ┌───────────┼───────────┐\n ▼ ▼ ▼\n preinstall.sh bin/* postinstall.sh\n (packages) → /usr/local/bin (config + services)\n```\n\n| Phase | Script | Responsibility |", + "truncated": false, + "loaded": [], + "display": { + "type": "file", + "path": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/DEV.md", + "text": "# Development Guide\n\nHow this repo works, how to add features, and what to keep in mind when editing.\n\n---\n\n## Architecture\n\n### Installation Phases\n\n```\n install.sh\n │\n ┌───────────┼───────────┐\n ▼ ▼ ▼\n preinstall.sh bin/* postinstall.sh\n (packages) → /usr/local/bin (config + services)\n```\n\n| Phase | Script | Responsibility |\n|-------|--------|----------------|\n| Pre | `preinstall.sh` | System packages, apt repos, global binaries (yt-dlp) |\n| Install | `install.sh` | Copies `bin/*` → `/usr/local/bin/` (chmod 755), `lib/common.sh` → `/usr/local/bin/common.sh` |\n| Post | `postinstall.sh` | User config (SSH keys, PATH, bash completion), systemd services |\n\nEach phase is independent and runs only if the corresponding script exists.\n\n### Directory Layout\n\n| Directory | Purpose | Installed To |\n|-----------|---------|-------------|\n| `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |\n| `apps//` | Optional desktop app installers | run on demand |\n| `lib/` | Shared library (`common.sh`) | sourced at build time |\n| `config/` | Gitignored user config files | `~/.config//` (via postinstall) |\n| `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` |\n| `systemd/` | Systemd unit files | `/etc/systemd/system/` (via postinstall) |\n\n### The `pos` CLI\n\n`bin/pos` is a smart dispatcher. It scans its own directory for executable `pos-*` files and uses variable-length argument matching:\n\n```\npos docker compose up jellyfin\n → tries pos-docker-compose-up-jellyfin (not found)\n → tries pos-docker-compose-up (not found)\n → finds pos-docker-compose → runs with args \"up jellyfin\"\n```\n\nAll non-interactive commands log to `~/.local/share/linux_post_install/logs/`.\n\n### Shared Library (`lib/common.sh`)\n\nSourced by most scripts. Key functions:\n\n| Function | Purpose |\n|----------|---------|\n| `log \"msg\"` | Green `[+]` status message |\n| `warn \"msg\"` | Yellow `[!]` warning |\n| `err \"msg\"` | Red `ERROR:` + exit 1 |\n| `ok \"msg\"` | Green `OK` prefix |\n| `section \"title\"` | Cyan-bordered section header |\n| `step N T \"msg\"` | Numbered step header |\n| `run cmd` | Executes command, respects `$DRY_RUN` |\n| `spawn \"msg\" cmd` | Animated braille spinner + elapsed time |\n| `timer_start` / `timer_stop` | Elapsed time tracking |\n| `confirm \"prompt\"` | y/N prompt with optional default |\n\n---\n\n## Adding a New CLI Tool\n\n### 1. Create the script\n\n```bash\n#!/usr/bin/env bash\n\nset -euo pipefail\n\nsource \"$(dirname \"$0\")/../lib/common.sh\"\n\nusage() {\n cat <\nEOF\n exit 0\n}\n\ncase \"${1:-}\" in\n -h|--help|\"\") usage ;;\nesac\n\n# --- script logic ---\n```\n\n**Conventions:**\n- Shebang: `#!/usr/bin/env bash`\n- Strict mode: `set -euo pipefail`\n- `--help` flag: accept `-h` / `--help` via `case` pattern\n- Shared library: always source `common.sh` for colors, logging, spinners\n- Exit codes: `0` success, `1` error\n- No shared lib? Inline fallbacks:\n ```bash\n log() { echo \"[+] $*\"; }\n warn() { echo \"[!] $*\"; }\n err() { echo \"ERROR: $*\" >&2; exit 1; }\n ```\n\n### 2. Add system dependencies\n\nAdd package names to the `PACKAGES` array in `preinstall.sh`:\n\n```bash\nPACKAGES=(\n ...\n your-package\n)\n```\n\n### 3. Add config files (if needed)\n\nPlace defaults in `config/` and add copy logic to `postinstall.sh`. If they contain secrets, add to `.gitignore` and document in README.\n\n### 4. Add SSH keys (if needed)\n\nPlace public keys in `config/authorized_keys` (one per line). `postinstall.sh` reads this file automatically.\n\n### 5. Update README\n\nAdd a section under the relevant category in README.md.\n\n### 6. Test\n\n```bash\nbash -n bin/your-tool\nshellcheck bin/your-tool\n./bin/your-tool --help\n```\n\n---\n\n## Adding an Optional App\n\n### 1. Create the installer\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\nsource \"$(dirname \"$0\")/../../lib/common.sh\"\n\ninstall_myapp() {\n command -v myapp &>/dev/null && { log \"myapp already installed\"; return 0; }\n spawn \"Installing myapp\" sudo apt install -y myapp\n}\n\ninstall_myapp\n```\n\nPlace it in `apps//.sh`. It auto-appears in the picker — no registration needed.\n\n**Categories:** `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`\n\n### 2. Conventions\n\n- Idempotent: check `command -v` before installing\n- APT packages → `sudo apt install -y` inside `spawn`\n- Official scripts → `curl ... | sh` inside `spawn`\n- Flatpak → `flatpak install -y flathub ` inside `spawn`\n- `.deb` files → download to temp, `sudo apt install -y ./file.deb` inside `spawn`\n- `usermod` for groups → print re-login reminder\n\n---\n\n## Editing an Existing Tool\n\n1. Find the script in `bin/`\n2. Understand its contract (args, output, exit codes)\n3. Make the change — keep it idempotent\n4. Update README if behaviour changed\n5. Run `shellcheck` on the modified file\n\n---\n\n## Best Practices\n\n### Idempotency\n\nCheck before creating, use `>>` with grep guards, don't overwrite user configs.\n\n### Error Handling\n\n```bash\nset -euo pipefail\ncommand -v docker &>/dev/null || { echo \"docker not found\"; exit 1; }\n[[ -n \"${1:-}\" ]] || { echo \"Usage: my-tool \"; exit 1; }\n```\n\n### Portability\n\nTargets **Debian** and **Ubuntu**. Use `apt`, assume bash at `/usr/bin/env bash`, check tools with `command -v`.\n\n### Dry-run Support\n\nScripts support `--dry-run`. Use the `run()` helper:\n\n```bash\nrun() {\n if [ \"$DRY_RUN\" -eq 1 ]; then\n log \"(dry-run) $*\"\n else\n \"$@\"\n fi\n}\nrun sudo apt install -y git\n```\n\n### Security\n\n- Never hardcode secrets — put them in `config/` (gitignored)\n- `chmod 600` for sensitive files\n- Validate input before shell commands\n- Use `sudo` only where needed\n\n### Naming\n\n- CLI tools: `bin/pos--`\n- Legacy wrappers: `bin/wr-*`\n- App installers: `apps//.sh`\n- Lowercase with hyphens\n\n---\n\n## Working with Systemd\n\nCreate `systemd/.service` — `postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically.\n\n```ini\n[Unit]\nDescription=My Service\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/your-script.sh\nRestart=on-failure\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n```\n\n---\n\n## Working with Config Files\n\n1. Place the file in `config/`\n2. Add copy logic to `postinstall.sh`:\n\n```bash\nif [ -f config/your-config.conf ]; then\n mkdir -p \"$HOME/.config/your-app\"\n cp config/your-config.conf \"$HOME/.config/your-app/your-config.conf\"\n chmod 600 \"$HOME/.config/your-app/your-config.conf\"\nfi\n```\n\n---\n\n## Docker Compose / ScaleTail\n\nThe 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. Each service gets a `tail-xxxxx.ts.net` URL via `network_mode: service:tailscale`.\n\n### Config Strategy — Three Layers\n\nValues cascade from least to most specific:\n\n```\nTemplate .env (per-service defaults from ScaleTail)\n ↓\nGlobal config (~/.config/linux_post_install/compose.env)\n ↓\nPer-service .env (/srv//.env) — created on first deploy, NEVER overwritten\n```\n\nOn first `pos docker compose up `:\n1. Template `.env` is copied to `/srv//.env`\n2. Matching keys from global config are filled in\n3. If `TS_AUTHKEY` is still empty, you're prompted to enter it\n4. After that, the per-service `.env` is **never touched** — not even by `update`\n\n### Layout\n\n| Path | Purpose | Mutability |\n|------|---------|------------|\n| `/usr/local/share/linux_post_install/scale-tail/services//` | ScaleTail templates (git repo) | Read-only |\n| `~/.config/linux_post_install/compose.env` | Your global defaults | Edit via `config set` or `config edit` |\n| `/srv//` | Active deployment | Per-service `.env` preserved forever |\n\n### Key Commands\n\n| Command | Behaviour |\n|---------|-----------|\n| `pos docker compose up ` | Deploys to `$SERVICES_BASE//`, creates `config/` + `data/`, generates `.env` from global defaults |\n| `pos docker compose down ` | Stops the stack |\n| `pos docker compose update` | `git pull` templates + refreshes `compose.yaml` for all deployed services (`.env` untouched) |\n| `pos docker compose config set K=V` | Sets a global default in `~/.config/linux_post_install/compose.env` |\n| `pos docker compose config show` | Displays current global config and `SERVICES_BASE` |\n| `pos docker compose config edit` | Opens global config in `$EDITOR` |\n\n### Global Config Keys\n\n| Key | Required | Default | Purpose |\n|-----|----------|---------|---------|\n| `TS_AUTHKEY` | Yes | — | Tailscale auth key for sidecar networking |\n| `TZ` | No | `Europe/Amsterdam` | Timezone for services |\n| `DNS_SERVER` | No | `9.9.9.9` | Custom DNS server |\n| `SERVICES_BASE` | No | `/srv` | Root directory for all deployments |\n\n---\n\n## Commit Guidelines\n\n- Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`\n- Explain *why*, not just *what*\n- One logical change per commit\n\n```\nfeat: add pos-disk-usage for monitoring disk space\nfix: pos-network-ip fails when no default route exists\ndocs: add example output for pos-network-scan\n```\n\n---\n\n## Useful Commands\n\n```bash\n# Syntax check a single script\nbash -n bin/my-script\n\n# ShellCheck linting\nshellcheck bin/my-script\n\n# Check all scripts\nfor f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do\n bash -n \"$f\" || echo \"FAIL: $f\"\ndone\n\n# Init submodule\ngit submodule update --init\n\n# Pull latest ScaleTail templates\ngit submodule update --remote compose/scale-tail\n\n# Test install in Docker\ndocker run --rm -it -v $PWD:/repo ubuntu:22.04 bash\n# inside: cd /repo && ./install.sh\n\n# Test app installers\n./apps/install.sh\n./apps/install.sh --all\n./apps/install.sh docker vscode\n```", + "lineStart": 1, + "lineEnd": 364, + "totalLines": 364, + "truncated": false + } + }, + "title": "DEV.md", + "time": { + "start": 1785353912508, + "end": 1785353912712 + } + }, + "id": "prt_faf62bfe1001mmUH23616tV3xU", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + }, + { + "type": "tool", + "tool": "read", + "callID": "call_03_TKz1oAfBfiDSGEhCNptW3478", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/algorithm.md" + }, + "output": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/algorithm.md\nfile\n\n1: # Algorithm Diagrams\n2: \n3: ---\n4: \n5: ## 1. Installation Flow\n6: \n7: ```\n8: User runs: ./install.sh [--skip ] [--steps ] [--dry-run]\n9: │\n10: ▼\n11: ┌─────────────────────┐\n12: │ Parse CLI flags │\n13: │ --skip, --steps, │\n14: │ --dry-run, --apps │\n15: └─────────┬───────────┘\n16: │\n17: ▼\n18: ┌─ should_run(1, preinstall) ?\n19: │ YES NO\n20: ▼ ▼\n21: ┌────────────────┐ ┌──────────────┐\n22: │ preinstall.sh │ │ skip │\n23: │ apt update │ │ │\n24: │ 25+ packages │ │ │\n25: │ yt-dlp │ │ │\n26: │ fail2ban │ │ │\n27: └───────┬────────┘ └──────────────┘\n28: │\n29: ▼\n30: ┌─ should_run(2, scripts) ?\n31: │ YES NO\n32: ▼ ▼\n33: ┌────────────────┐ ────────\n34: │ Copy bin/* │\n35: │ → /usr/local │\n36: │ Copy lib/ │\n37: │ → common.sh │\n38: └───────┬────────┘\n39: │\n40: ▼\n41: ┌─ should_run(3, postinstall) ?\n42: │ YES NO\n43: ▼ ▼\n44: ┌──────────────────────────┐\n45: │ postinstall.sh │\n46: │ • fail2ban config │\n47: │ • ~/.bashrc PATH │\n48: │ • bash completion │\n49: │ • systemd services │\n50: │ • SSH authorized_keys │\n51: └──────────┬───────────────┘\n52: │\n53: ▼\n54: ┌─ should_run(4, scalepoint) ?\n55: │ YES NO\n56: ▼ ▼\n57: ┌──────────────────────────┐\n58: │ Clone ScaleTail │\n59: │ → /usr/local/share/ │\n60: │ linux_post_install/ │\n61: └──────────┬───────────────┘\n62: │\n63: ▼\n64: ┌─ RUN_APPS ?\n65: │ ┌── 1 (--apps) ─── interactive picker\n66: │ ├── 2 (--full) ─── install all\n67: │ └── else ─── skip\n68: ▼\n69: ┌──────────────────────┐\n70: │ Bootstrap complete │\n71: └──────────────────────┘\n72: ```\n73: \n74: ---\n75: \n76: ## 2. `pos` CLI Dispatch\n77: \n78: ```\n79: User: pos docker compose up jellyfin\n80: │\n81: ▼\n82: ┌──────────────────────┐\n83: │ args = [docker, │\n84: │ compose, │\n85: │ up, │\n86: │ jellyfin] │\n87: │ n = 4 │\n88: └──────────┬───────────┘\n89: │\n90: ▼\n91: ┌──────────────────────┐\n92: │ Loop i from n-1 to 0 │\n93: │ Build candidate: │\n94: │ pos-args[0..i] │\n95: └──────────┬───────────┘\n96: │\n97: ┌──────────┴──────────┐\n98: │ i=3: pos-up-jellyfin│ ✗ not found\n99: │ i=2: pos-up │ ✗ not found\n100: │ i=1: pos-compose-up │ ✗ not found\n101: │ i=0: pos-docker │ ✗ not found\n102: │ │\n103: │ Fallback to │\n104: │ pos-docker-compose │ ✓ found!\n105: │ Remaining args: │\n106: │ [up, jellyfin] │\n107: └──────────┬──────────┘\n108: │\n109: ▼\n110: ┌───────────────┐\n111: │ Is this an │\n112: │ interactive │\n113: │ command? │\n114: │ (system- │\n115: │ firewall, │\n116: │ media-mp4) │\n117: └──┬────────┬───┘\n118: │ YES │ NO\n119: ▼ ▼\n120: ┌────────┐ ┌──────────────┐\n121: │ Log │ │ Run cmd │\n122: │ invoc. │ │ + tee to │\n123: │ only │ │ log file │\n124: │ exec │ │ + log rc │\n125: │ direct │ │ exit rc │\n126: └────────┘ └──────────────┘\n127: ```\n128: \n129: ---\n130: \n131: ## 3. Docker Compose `up` Algorithm\n132: \n133: ```\n134: pos docker compose up qbittorrent\n135: │\n136: ▼\n137: ┌──────────────────────┐\n138: │ check_deps: docker │ ──✗──→ ERROR\n139: │ installed? │\n140: └──────────┬───────────┘\n141: │ ✓\n142: ▼\n143: ┌──────────────────────┐\n144: │ check_templates: │ ──✗──→ ERROR\n145: │ ScaleTail dir exist? │\n146: └──────────┬───────────┘\n147: │ ✓\n148: ▼\n149: ┌──────────────────────┐\n150: │ check_service: │ ──✗──→ ERROR\n151: │ template for │\n152: │ qbittorrent exists? │\n153: └──────────┬───────────┘\n154: │ ✓\n155: ▼\n156: ┌──────────────────────┐\n157: │ load_global_config │\n158: │ SERVICES_BASE=/srv │\n159: │ target=/srv/ │\n160: │ qbittorrent │\n161: └──────────┬───────────┘\n162: │\n163: ▼\n164: ┌──────────────┐\n165: │ /srv/ │\n166: │ qbittorrent/ │\n167: │ exists? │\n168: └──┬───────┬───┘\n169: NO YES\n170: │ │\n171: ▼ ▼\n172: ┌────────┐ ┌──────────┐\n173: │ Create │ │ mkdir -p │\n174: │ dirs │ │ config/ │\n175: │ copy │ │ data/ │\n176: │ compose│ │ │\n177: │ .yaml │ │ │\n178: └───┬────┘ └──────────┘\n179: │ │\n180: └─────┬─────┘\n181: │\n182: ▼\n183: ┌──────────────┐\n184: │ .env file │\n185: │ exists? │\n186: └──┬───────┬───┘\n187: NO YES\n188: │ │\n189: │ ▼\n190: │ (skip to\n191: │ startup)\n192: ▼\n193: ┌──────────────────────────┐\n194: │ Copy template .env or │\n195: │ create from defaults │\n196: │ │\n197: │ Fill in global values: │\n198: │ • TS_AUTHKEY ← global │\n199: │ • TZ ← global │\n200: │ • DNS_SERVER ← global │\n201: │ │\n202: │ If TS_AUTHKEY empty: │\n203: │ prompt user │\n204: └──────────┬───────────────┘\n205: │\n206: ▼\n207: ┌──────────────┐\n208: │ Prompt: │\n209: │ \"Edit .env │\n210: │ before │\n211: │ starting?\" │\n212: └──┬───────┬───┘\n213: YES NO\n214: │ │\n215: ▼ ▼\n216: ┌────────┐ ┌──────────┐\n217: │ Open │ │ Continue │\n218: │ $EDITOR│ │ │\n219: │ (nano) │ │ │\n220: └───┬────┘ └──────────┘\n221: │ │\n222: └─────┬─────┘\n223: │\n224: ▼\n225: ┌──────────────────────┐\n226: │ docker compose up -d │\n227: │ (in /srv/qbittorrent)│\n228: └──────────┬───────────┘\n229: │\n230: ▼\n231: ┌──────────────────────┐\n232: │ \"qbittorrent is │\n233: │ running\" │\n234: └──────────────────────┘\n235: ```\n236: \n237: ---\n238: \n239: ## 4. Config Cascade (Three Layers)\n240: \n241: ```\n242: USER\n243: │\n244: ┌──────────────┴──────────────┐\n245: │ sets via │\n246: │ pos docker compose config │\n247: │ set TS_AUTHKEY=... │\n248: ▼ │\n249: ┌────────────────────┐ │\n250: │ Layer 2: Global │◄────────────────┘\n251: │ ~/.config/ │\n252: │ linux_post_install│\n253: │ /compose.env │\n254: │ │\n255: │ TS_AUTHKEY=... │\n256: │ TZ=Europe/... │\n257: │ DNS_SERVER=9.9... │\n258: │ SERVICES_BASE=/srv│\n259: └────────┬───────────┘\n260: │\n261: │ values propagate to new deployments\n262: ▼\n263: ┌────────────────────┐\n264: │ Layer 1: Template │\n265: │ /usr/local/share/ │\n266: │ linux_post_install│\n267: │ /scale-tail/ │\n268: │ services// │\n269: │ .env │\n270: │ │\n271: │ SERVICE=xxx │\n272: │ IMAGE_URL= │\n273: │ SERVICEPORT= │\n274: └────────┬───────────┘\n275: │\n276: │ merged on first \"up\"\n277: ▼\n278: ┌────────────────────┐\n279: │ Layer 3: Per-svc │\n280: │ /srv// │\n281: │ .env │\n282: │ │\n283: │ ─── NEVER ─── │\n284: │ ─── TOUCHED ─── │\n285: │ ─── AFTER ───── │\n286: └────────────────────┘\n287: │\n288: ▼\n289: ┌────────────────────┐\n290: │ docker compose │\n291: │ up -d │\n292: │ │\n293: │ update only │\n294: │ refreshes │\n295: │ compose.yaml, │\n296: │ NOT .env │\n297: └────────────────────┘\n298: ```\n299: \n300: ---\n301: \n302: ## 5. Logging System\n303: \n304: ```\n305: pos docker compose up jellyfin\n306: │\n307: ├── Is it an interactive command?\n308: │ (system-firewall, media-mp4)\n309: │\n310: ├── YES ──→ log: \"pos system firewall → exit 0\"\n311: │ exec: run directly (no tee)\n312: │\n313: └── NO ───→ LOG_DIR=~/.local/share/linux_post_install/logs/\n314: │\n315: ├── CMD_SAFE = docker_compose_up_jellyfin\n316: │\n317: ├── LOG_FILE = 20260728_120000_pos_docker_compose_up_jellyfin.log\n318: │ (full stdout+stderr via tee)\n319: │\n320: ├── MAIN_LOG = pos.log\n321: │ (appended: timestamp + command + logfile + exit code)\n322: │\n323: └── exit with command's exit code\n324: ```\n325: \n326: ---\n327: \n328: ## 6. `pos-vbox` Lifecycle\n329: \n330: ```\n331: create lab1\n332: │\n333: ├── docker run -d --name lab1\n334: │ --label linux_post_install.vbox=true\n335: │ [--volume $PWD:/workspace] ← --dir .\n336: │ image: ubuntu:latest sleep infinity\n337: │\n338: ├── \"Enter now? [Y/n]\"\n339: │ ├── Y → docker exec -it lab1 bash\n340: │ └── n → exit\n341: │\n342: └── Container is labeled → ls filters by label\n343: \n344: enter lab1\n345: │\n346: ├── docker start lab1 ← auto-starts if stopped\n347: ├── Detect bind mount from labels\n348: └── docker exec -it lab1 bash\n349: \n350: ls\n351: └── docker ps -a --filter label=linux_post_install.vbox=true\n352: ```\n\n(End of file - total 352 lines)\n", + "metadata": { + "preview": "# Algorithm Diagrams\n\n---\n\n## 1. Installation Flow\n\n```\nUser runs: ./install.sh [--skip ] [--steps ] [--dry-run]\n │\n ▼\n ┌─────────────────────┐\n │ Parse CLI flags │\n │ --skip, --steps, │\n │ --dry-run, --apps │\n └─────────┬───────────┘\n │\n ▼\n ┌─ should_run(1, preinstall) ?\n │ YES NO\n ▼ ▼", + "truncated": false, + "loaded": [], + "display": { + "type": "file", + "path": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/algorithm.md", + "text": "# Algorithm Diagrams\n\n---\n\n## 1. Installation Flow\n\n```\nUser runs: ./install.sh [--skip ] [--steps ] [--dry-run]\n │\n ▼\n ┌─────────────────────┐\n │ Parse CLI flags │\n │ --skip, --steps, │\n │ --dry-run, --apps │\n └─────────┬───────────┘\n │\n ▼\n ┌─ should_run(1, preinstall) ?\n │ YES NO\n ▼ ▼\n ┌────────────────┐ ┌──────────────┐\n │ preinstall.sh │ │ skip │\n │ apt update │ │ │\n │ 25+ packages │ │ │\n │ yt-dlp │ │ │\n │ fail2ban │ │ │\n └───────┬────────┘ └──────────────┘\n │\n ▼\n ┌─ should_run(2, scripts) ?\n │ YES NO\n ▼ ▼\n ┌────────────────┐ ────────\n │ Copy bin/* │\n │ → /usr/local │\n │ Copy lib/ │\n │ → common.sh │\n └───────┬────────┘\n │\n ▼\n ┌─ should_run(3, postinstall) ?\n │ YES NO\n ▼ ▼\n ┌──────────────────────────┐\n │ postinstall.sh │\n │ • fail2ban config │\n │ • ~/.bashrc PATH │\n │ • bash completion │\n │ • systemd services │\n │ • SSH authorized_keys │\n └──────────┬───────────────┘\n │\n ▼\n ┌─ should_run(4, scalepoint) ?\n │ YES NO\n ▼ ▼\n ┌──────────────────────────┐\n │ Clone ScaleTail │\n │ → /usr/local/share/ │\n │ linux_post_install/ │\n └──────────┬───────────────┘\n │\n ▼\n ┌─ RUN_APPS ?\n │ ┌── 1 (--apps) ─── interactive picker\n │ ├── 2 (--full) ─── install all\n │ └── else ─── skip\n ▼\n ┌──────────────────────┐\n │ Bootstrap complete │\n └──────────────────────┘\n```\n\n---\n\n## 2. `pos` CLI Dispatch\n\n```\nUser: pos docker compose up jellyfin\n │\n ▼\n ┌──────────────────────┐\n │ args = [docker, │\n │ compose, │\n │ up, │\n │ jellyfin] │\n │ n = 4 │\n └──────────┬───────────┘\n │\n ▼\n ┌──────────────────────┐\n │ Loop i from n-1 to 0 │\n │ Build candidate: │\n │ pos-args[0..i] │\n └──────────┬───────────┘\n │\n ┌──────────┴──────────┐\n │ i=3: pos-up-jellyfin│ ✗ not found\n │ i=2: pos-up │ ✗ not found\n │ i=1: pos-compose-up │ ✗ not found\n │ i=0: pos-docker │ ✗ not found\n │ │\n │ Fallback to │\n │ pos-docker-compose │ ✓ found!\n │ Remaining args: │\n │ [up, jellyfin] │\n └──────────┬──────────┘\n │\n ▼\n ┌───────────────┐\n │ Is this an │\n │ interactive │\n │ command? │\n │ (system- │\n │ firewall, │\n │ media-mp4) │\n └──┬────────┬───┘\n │ YES │ NO\n ▼ ▼\n ┌────────┐ ┌──────────────┐\n │ Log │ │ Run cmd │\n │ invoc. │ │ + tee to │\n │ only │ │ log file │\n │ exec │ │ + log rc │\n │ direct │ │ exit rc │\n └────────┘ └──────────────┘\n```\n\n---\n\n## 3. Docker Compose `up` Algorithm\n\n```\npos docker compose up qbittorrent\n │\n ▼\n ┌──────────────────────┐\n │ check_deps: docker │ ──✗──→ ERROR\n │ installed? │\n └──────────┬───────────┘\n │ ✓\n ▼\n ┌──────────────────────┐\n │ check_templates: │ ──✗──→ ERROR\n │ ScaleTail dir exist? │\n └──────────┬───────────┘\n │ ✓\n ▼\n ┌──────────────────────┐\n │ check_service: │ ──✗──→ ERROR\n │ template for │\n │ qbittorrent exists? │\n └──────────┬───────────┘\n │ ✓\n ▼\n ┌──────────────────────┐\n │ load_global_config │\n │ SERVICES_BASE=/srv │\n │ target=/srv/ │\n │ qbittorrent │\n └──────────┬───────────┘\n │\n ▼\n ┌──────────────┐\n │ /srv/ │\n │ qbittorrent/ │\n │ exists? │\n └──┬───────┬───┘\n NO YES\n │ │\n ▼ ▼\n ┌────────┐ ┌──────────┐\n │ Create │ │ mkdir -p │\n │ dirs │ │ config/ │\n │ copy │ │ data/ │\n │ compose│ │ │\n │ .yaml │ │ │\n └───┬────┘ └──────────┘\n │ │\n └─────┬─────┘\n │\n ▼\n ┌──────────────┐\n │ .env file │\n │ exists? │\n └──┬───────┬───┘\n NO YES\n │ │\n │ ▼\n │ (skip to\n │ startup)\n ▼\n ┌──────────────────────────┐\n │ Copy template .env or │\n │ create from defaults │\n │ │\n │ Fill in global values: │\n │ • TS_AUTHKEY ← global │\n │ • TZ ← global │\n │ • DNS_SERVER ← global │\n │ │\n │ If TS_AUTHKEY empty: │\n │ prompt user │\n └──────────┬───────────────┘\n │\n ▼\n ┌──────────────┐\n │ Prompt: │\n │ \"Edit .env │\n │ before │\n │ starting?\" │\n └──┬───────┬───┘\n YES NO\n │ │\n ▼ ▼\n ┌────────┐ ┌──────────┐\n │ Open │ │ Continue │\n │ $EDITOR│ │ │\n │ (nano) │ │ │\n └───┬────┘ └──────────┘\n │ │\n └─────┬─────┘\n │\n ▼\n ┌──────────────────────┐\n │ docker compose up -d │\n │ (in /srv/qbittorrent)│\n └──────────┬───────────┘\n │\n ▼\n ┌──────────────────────┐\n │ \"qbittorrent is │\n │ running\" │\n └──────────────────────┘\n```\n\n---\n\n## 4. Config Cascade (Three Layers)\n\n```\n USER\n │\n ┌──────────────┴──────────────┐\n │ sets via │\n │ pos docker compose config │\n │ set TS_AUTHKEY=... │\n ▼ │\n ┌────────────────────┐ │\n │ Layer 2: Global │◄────────────────┘\n │ ~/.config/ │\n │ linux_post_install│\n │ /compose.env │\n │ │\n │ TS_AUTHKEY=... │\n │ TZ=Europe/... │\n │ DNS_SERVER=9.9... │\n │ SERVICES_BASE=/srv│\n └────────┬───────────┘\n │\n │ values propagate to new deployments\n ▼\n ┌────────────────────┐\n │ Layer 1: Template │\n │ /usr/local/share/ │\n │ linux_post_install│\n │ /scale-tail/ │\n │ services// │\n │ .env │\n │ │\n │ SERVICE=xxx │\n │ IMAGE_URL= │\n │ SERVICEPORT= │\n └────────┬───────────┘\n │\n │ merged on first \"up\"\n ▼\n ┌────────────────────┐\n │ Layer 3: Per-svc │\n │ /srv// │\n │ .env │\n │ │\n │ ─── NEVER ─── │\n │ ─── TOUCHED ─── │\n │ ─── AFTER ───── │\n └────────────────────┘\n │\n ▼\n ┌────────────────────┐\n │ docker compose │\n │ up -d │\n │ │\n │ update only │\n │ refreshes │\n │ compose.yaml, │\n │ NOT .env │\n └────────────────────┘\n```\n\n---\n\n## 5. Logging System\n\n```\npos docker compose up jellyfin\n │\n ├── Is it an interactive command?\n │ (system-firewall, media-mp4)\n │\n ├── YES ──→ log: \"pos system firewall → exit 0\"\n │ exec: run directly (no tee)\n │\n └── NO ───→ LOG_DIR=~/.local/share/linux_post_install/logs/\n │\n ├── CMD_SAFE = docker_compose_up_jellyfin\n │\n ├── LOG_FILE = 20260728_120000_pos_docker_compose_up_jellyfin.log\n │ (full stdout+stderr via tee)\n │\n ├── MAIN_LOG = pos.log\n │ (appended: timestamp + command + logfile + exit code)\n │\n └── exit with command's exit code\n```\n\n---\n\n## 6. `pos-vbox` Lifecycle\n\n```\ncreate lab1\n │\n ├── docker run -d --name lab1\n │ --label linux_post_install.vbox=true\n │ [--volume $PWD:/workspace] ← --dir .\n │ image: ubuntu:latest sleep infinity\n │\n ├── \"Enter now? [Y/n]\"\n │ ├── Y → docker exec -it lab1 bash\n │ └── n → exit\n │\n └── Container is labeled → ls filters by label\n\nenter lab1\n │\n ├── docker start lab1 ← auto-starts if stopped\n ├── Detect bind mount from labels\n └── docker exec -it lab1 bash\n\nls\n └── docker ps -a --filter label=linux_post_install.vbox=true\n```", + "lineStart": 1, + "lineEnd": 352, + "totalLines": 352, + "truncated": false + } + }, + "title": "algorithm.md", + "time": { + "start": 1785353912725, + "end": 1785353912893 + } + }, + "id": "prt_faf62c0e2001jH9JfXmJGPEc40", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + }, + { + "reason": "tool-calls", + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-finish", + "tokens": { + "total": 16072, + "input": 6901, + "output": 215, + "reasoning": 252, + "cache": { + "write": 0, + "read": 8704 + } + }, + "cost": 0, + "id": "prt_faf62c5af001KD7VnPb20fcIie", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62abce001vJqgMnbuKhau56" + } + ] + }, + { + "info": { + "parentID": "msg_faf612623001UNz7GERJtr7i46", + "role": "assistant", + "mode": "plan", + "agent": "plan", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 32281, + "input": 15289, + "output": 371, + "reasoning": 621, + "cache": { + "write": 0, + "read": 16000 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353914664, + "completed": 1785353935635 + }, + "finish": "tool-calls", + "id": "msg_faf62c928002Yr3Ld6vfUbXXqb", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-start", + "id": "prt_faf62d1ea00193ZQEpXpu1i1HN", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62c928002Yr3Ld6vfUbXXqb" + }, + { + "type": "reasoning", + "text": "The user wants me to either create a doc or use the existing AGENT_Context_Project.md as a reference for my knowledge of this repo. Since the file already exists and is quite comprehensive (426 lines), I should:\n\n1. Read the existing AGENT_Context_Project.md\n2. Evaluate if it needs any updates or improvements\n3. Suggest any changes\n\nBut since I'm in plan mode, I should note what I've found and ask the user what they'd like to do. The existing context document is already very thorough. Let me think about what might be missing or could be improved:\n\nLooking at the existing AGENT_Context_Project.md vs what the exploration agent found, I notice a few gaps:\n\n1. The AGENT.md says `pos-docker-compose` is 317 lines but it's actually 363 lines\n2. The AGENT.md says `pos` is ~130 lines but it's 145 lines\n3. The AGENT.md says `bin/pos-system-firewall` is 284 lines (correct)\n4. The AGENT.md says `postinstall.sh` is ~65 lines but it's 90 lines\n5. The AGENT.md says `install.sh` is ~120 lines but it's 144 lines\n6. The AGENT.md says `apps/install.sh` is 99 lines but it's 143 lines\n7. The AGENT.md says `pos-vbox` is ~160 lines but it's 156 lines\n\nThere are also some missing items from the directory tree (like `config/authorized_keys` showing in the tree) and some accuracy issues.\n\nBut more importantly - the user said \"create doc or use agent context doc to help you for be reference of your knowledge of this repo project\". They seem to want me to make sure I'm fully knowledgeable about this project. Since the existing AGENT_Context_Project.md file already exists and is in the repo, I should acknowledge it and perhaps suggest any updates needed.\n\nActually, let me re-read the user's message: \"you are now founded for this repo, you are will known every single line in this project so create doc or use agent context doc to help you for be refernce of your knowledge of this repo project\"\n\nI think they're saying: \"You are now 'founded' for this repo (as in, you're grounded/anchored to this repo), and you will know every single line in this project, so create a doc (or use the agent context doc) to help you as a reference for your knowledge of this repo project.\"\n\nSo they want me to create or use an agent context document. Since one already exists (`AGENT_Context_Project.md`), I should confirm with the user that I'll use this existing document as my reference, and perhaps suggest any updates to bring it in line with the actual codebase.\n\nLet me ask the user what they'd like to do - use the existing document as-is, update it, or create something different.", + "time": { + "start": 1785353917789, + "end": 1785353922281 + }, + "id": "prt_faf62d55c001WGzc2mOVLEqvri", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62c928002Yr3Ld6vfUbXXqb" + }, + { + "type": "text", + "text": "I've thoroughly reviewed the repository. The AGENT_Context_Project.md (426 lines) already exists and is comprehensive but has some inaccuracies vs the actual codebase:\n\n**Discrepancies found:**\n- `pos-docker-compose`: doc says 317 lines → actually **363 lines**\n- `postinstall.sh`: doc says ~65 lines → actually **90 lines**\n- `install.sh`: doc says ~120 lines → actually **144 lines**\n- `apps/install.sh`: doc says 99 lines → actually **143 lines**\n- Missing entries: `config/authorized_keys`, `bin/autostart.sh` not in file table\n\nThe doc structure covers: Project Overview, Directory Structure, Installation Flow, `pos` CLI, Shared Library, Docker Compose/ScaleTail, Optional Apps, Systemd, Config Files, Coding Conventions, Dev Workflow, Quick Reference, and Common Tasks.", + "time": { + "start": 1785353922286, + "end": 1785353924755 + }, + "id": "prt_faf62e6ee0010KLxpcNvuRvqse", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62c928002Yr3Ld6vfUbXXqb" + }, + { + "type": "tool", + "tool": "question", + "callID": "call_00_XrBKjV66vCYgcbDr409g8966", + "state": { + "status": "completed", + "input": { + "questions": [ + { + "question": "The AGENT_Context_Project.md already exists with good coverage but has a few size inaccuracies. What would you like me to do?", + "header": "Agent Context Doc", + "options": [ + { + "label": "Update existing doc (Recommended)", + "description": "Fix line count inaccuracies, add missing entries (autostart.sh, config/authorized_keys), and sync with actual codebase" + }, + { + "label": "Use as-is", + "description": "Keep the current doc unchanged and use it as reference" + }, + { + "label": "Create a new condensed version", + "description": "Create a smaller, optimized context file from scratch" + } + ] + } + ] + }, + "output": "User has answered your questions: \"The AGENT_Context_Project.md already exists with good coverage but has a few size inaccuracies. What would you like me to do?\"=\"Update existing doc (Recommended)\". You can now continue with the user's answers in mind.", + "metadata": { + "answers": [ + [ + "Update existing doc (Recommended)" + ] + ], + "truncated": false + }, + "title": "Asked 1 question", + "time": { + "start": 1785353924747, + "end": 1785353933895 + } + }, + "id": "prt_faf62ec32001e24ZXzjaXB4L1N", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62c928002Yr3Ld6vfUbXXqb" + }, + { + "reason": "tool-calls", + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-finish", + "tokens": { + "total": 32281, + "input": 15289, + "output": 371, + "reasoning": 621, + "cache": { + "write": 0, + "read": 16000 + } + }, + "cost": 0, + "id": "prt_faf63177a0016FqS8jjIsRnWzu", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf62c928002Yr3Ld6vfUbXXqb" + } + ] + }, + { + "info": { + "parentID": "msg_faf612623001UNz7GERJtr7i46", + "role": "assistant", + "mode": "plan", + "agent": "plan", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 32781, + "input": 1104, + "output": 293, + "reasoning": 152, + "cache": { + "write": 0, + "read": 31232 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353935647, + "completed": 1785353944630 + }, + "finish": "stop", + "id": "msg_faf631b1f0016B3cY8r077c1u4", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-start", + "id": "prt_faf6324380011PAWxNhjo5Hx0D", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf631b1f0016B3cY8r077c1u4" + }, + { + "type": "reasoning", + "text": "The user wants me to update the existing AGENT_Context_Project.md. Since I'm in plan mode (read-only), I need to create a plan for what I'll update. Let me outline the specific changes needed.\n\nWait, actually the system says I'm in \"Plan Mode\" with \"STRICTLY FORBIDDEN\" for any file edits. So I should present the plan for the user's approval before I can actually make changes. But the user already said \"Update existing doc (Recommended)\" - so they've given their intent. Let me check - am I still in Plan Mode? The system reminder says yes.\n\nSo I should present a detailed plan of what I'll change, and only after user approval can I execute.", + "time": { + "start": 1785353939376, + "end": 1785353940768 + }, + "id": "prt_faf6329b00019AEEC4BgKYtI3a", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf631b1f0016B3cY8r077c1u4" + }, + { + "type": "text", + "text": "Here's my plan to update the AGENT_Context_Project.md:\n\n**1. Fix line count inaccuracies:**\n| Section | Current (doc) | Actual |\n|---------|:-:|:-:|\n| `install.sh` | ~120 | 144 |\n| `postinstall.sh` | ~65 | 90 |\n| `bin/pos` | ~130 | 145 |\n| `pos-docker-compose` | 317 | 363 |\n| `pos-docker-health` | ~90 | 109 |\n| `pos-vbox` | ~160 | 156 |\n| `apps/install.sh` | 99 | 143 |\n\n**2. Add missing entries:**\n- Add `config/authorized_keys` to directory tree\n- Add `bin/autostart.sh` to directory tree and quick reference\n- Add note about `config/authorized_keys` being gitignored (alongside `config/rclone.conf`)\n\n**3. Minor corrections:**\n- `pos-vbox` default image is `ubuntu:22.04` (doc says `ubuntu:latest` in algorithm.md but not in context doc)\n- Note that `SCALE_DIR` constant lives in `pos-docker-compose` line 5\n\n**4. No structural changes needed** — the 13-section layout is well-organized and covers everything.\n\nShall I proceed with these updates?", + "time": { + "start": 1785353940772, + "end": 1785353943025 + }, + "id": "prt_faf632f240012olYrg7TL5ViiZ", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf631b1f0016B3cY8r077c1u4" + }, + { + "reason": "stop", + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-finish", + "tokens": { + "total": 32781, + "input": 1104, + "output": 293, + "reasoning": 152, + "cache": { + "write": 0, + "read": 31232 + } + }, + "cost": 0, + "id": "prt_faf633ad3001RStcHHoLPCgU4H", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf631b1f0016B3cY8r077c1u4" + } + ] + }, + { + "info": { + "role": "user", + "time": { + "created": 1785353946155 + }, + "agent": "build", + "model": { + "providerID": "opencode", + "modelID": "big-pickle" + }, + "summary": { + "diffs": [ + { + "file": "AGENT_Context_Project.md", + "patch": "Index: AGENT_Context_Project.md\n===================================================================\n--- AGENT_Context_Project.md\t\n+++ AGENT_Context_Project.md\t\n@@ -1,426 +1,429 @@\n # AGENT Context — Linux_post_install Project\n \n > **Purpose:** Single-source context document so any AI agent can understand the project, navigate the codebase, and make correct contributions.\n \n ---\n \n ## 1. Project Overview\n \n **Linux_post_install** is a personal bootstrap and homelab toolkit for Debian/Ubuntu. One command turns a bare install into a fully productive machine:\n \n - Automated system package installation (25+ packages)\n - A unified CLI (`pos`) for network, Docker, media, system, and SSH tasks\n - Optional desktop application installers (15 apps)\n - Docker Compose service management via ScaleTail templates (119+ self-hosted services with Tailscale sidecar)\n - Systemd service management for boot-time automation\n \n **Repository:** `https://gitea.skink-platy.ts.net/admin/Linux_post_install`\n **Target OS:** Debian / Ubuntu (uses `apt`)\n **Shell:** Bash (`#!/usr/bin/env bash`)\n \n ---\n \n ## 2. Directory Structure\n \n ```\n Linux_post_install/\n ├── install.sh # Main orchestrator — entry point\n ├── preinstall.sh # Phase 1: system packages via apt + yt-dlp\n ├── postinstall.sh # Phase 3: PATH, bash completion, systemd services\n │\n ├── lib/\n │ └── common.sh # Shared library (colors, logging, spinner, timer, run)\n │\n ├── bin/ # CLI tools — installed to /usr/local/bin/\n │ ├── pos # Main dispatcher — smart arg matching to pos-* scripts\n │ ├── pos-network-ip # Show interfaces, routes, public IP\n │ ├── pos-network-checkport # TCP port checker\n │ ├── pos-network-scan # Parallel ping sweep of CIDR subnet\n │ ├── pos-docker-ps # Enhanced docker ps (health, IPs, ports, uptime)\n │ ├── pos-docker-health # Quick one-glance health dashboard\n │ ├── pos-docker-compose # Docker Compose service manager (largest script, 317 lines)\n │ ├── pos-media-mp3 # Audio downloader (yt-dlp → MP3)\n │ ├── pos-media-mp4 # Video downloader (yt-dlp → MP4, interactive format select)\n │ ├── pos-system-firewall # Interactive UFW manager (menu-driven, 284 lines)\n │ ├── pos-ssh-load-keys # Load SSH keys into ssh-agent\n │ ├── pos-vbox # Disposable Docker-based \"VMs\"\n │ ├── autostart.sh # Boot-time script (via systemd)\n │ ├── wr-* # Legacy wrappers → pos (backward compat)\n │ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos\n │ └── ssh-load-all # Legacy wrapper → pos ssh load-keys\n │\n ├── apps/ # Optional desktop app installers (by category)\n │ ├── install.sh # Interactive picker / orchestrator\n │ ├── browsers/\n │ │ └── brave.sh # Brave Browser (APT repo)\n │ ├── development/\n │ │ ├── opencode.sh # opencode AI agent (official script)\n │ │ └── vscode.sh # VS Code (Microsoft APT repo)\n │ ├── media/\n │ │ ├── obs.sh # OBS Studio (apt)\n │ │ ├── scrcpy.sh # scrcpy Android mirror (GitHub release)\n │ │ └── vlc.sh # VLC media player (apt)\n │ ├── networking/\n │ │ ├── netbird.sh # NetBird VPN (official script)\n │ │ ├── tailscale.sh # Tailscale VPN (official script)\n │ │ └── zerotier.sh # ZeroTier VPN (official script)\n │ ├── remote-access/\n │ │ ├── termius.sh # Termius SSH client (.deb)\n │ │ └── vnc-viewer.sh # TigerVNC Viewer (apt)\n │ ├── system/\n │ │ ├── docker.sh # Docker Engine (get.docker.com)\n │ │ └── qemu.sh # QEMU + libvirt + KVM (apt)\n │ └── utilities/\n │ ├── affine.sh # AFFiNE knowledge base (AppImage)\n │ ├── btop.sh # btop resource monitor (apt)\n │ └── localsend.sh # LocalSend (flatpak)\n │\n ├── completions/\n │ └── pos.bash # Bash tab-completion for the pos CLI\n │\n+├── config/\n+│ └── authorized_keys # SSH public keys (gitignored)\n+│\n ├── compose/\n │ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n │\n ├── systemd/\n │ ├── autostart.service # Runs autostart.sh on boot\n │ └── ssh-agent.service # System-wide SSH agent socket\n │\n ├── README.md # User-facing documentation\n ├── DEV.md # Developer guide\n ├── .gitignore # Excludes secrets, Python artifacts, OS files\n └── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n ```\n \n ---\n \n ## 3. Installation Flow\n \n ```\n User runs: ./install.sh [--apps|--full|--dry-run|--skip |--steps ]\n │\n ├─ Phase 1: preinstall.sh (requires root)\n │ └─ apt update + installs 25+ packages + yt-dlp + fail2ban\n │\n ├─ Phase 2: install.sh (requires root)\n │ └─ Copies bin/* → /usr/local/bin/ (chmod 755)\n │ └─ Copies lib/common.sh → /usr/local/bin/common.sh (chmod 644)\n │\n ├─ Phase 3: postinstall.sh (runs as user)\n │ └─ Configures fail2ban (SSH jail: 5 retries, 1h ban)\n │ └─ PATH export in ~/.bashrc\n │ └─ Bash completion for pos CLI\n │ └─ Copies systemd/*.service → /etc/systemd/system/, enables them\n │\n ├─ Phase 4: ScaleTail clone\n │ └─ Shallow-clones ScaleTail templates to /usr/local/share/linux_post_install/scale-tail\n │\n └─ [if --apps or --full]: apps/install.sh\n └─ Interactive picker (or --all for non-interactive)\n ```\n \n **After install, the repo can be deleted** — all tools live in `/usr/local/bin/` and templates in `/usr/local/share/linux_post_install/`.\n \n ### install.sh Flags\n \n | Flag | Purpose |\n |------|---------|\n | `--apps` | Run interactive app picker after core install |\n | `--full` | Core install + all apps (non-interactive) |\n | `--dry-run` | Preview without executing |\n | `--skip ` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |\n | `--steps ` | Run only specific phases. Format: `1,3,4` or `1-3` |\n | `--no-color` | Disable colored output |\n \n ### pos Output Logging\n \n All non-interactive `pos` commands log output to `~/.local/share/linux_post_install/logs/`:\n - Per-command files: `YYYYMMDD_HHMMSS_pos_.log` (full stdout+stderr)\n - Main log: `pos.log` (command + timestamp + exit code for every invocation)\n - Interactive commands (`system-firewall`, `media-mp4`) only log invocation, not output\n \n ---\n \n ## 4. The `pos` CLI System\n \n ### How It Works\n \n `bin/pos` is the main dispatcher. It:\n 1. Scans its own directory for all executable `pos-*` files\n 2. Extracts category-subcommand names from filenames\n 3. Uses variable-length argument matching to find the right script\n \n **Example:** `pos docker compose up jellyfin`\n - Tries `pos-docker-compose-up-jellyfin` (not found)\n - Tries `pos-docker-compose-up` (not found)\n - Finds `pos-docker-compose` (runs with args `up jellyfin`)\n \n ### Available Commands\n \n | Category | Command | Script | Description |\n |----------|---------|--------|-------------|\n | network | ip | `pos-network-ip` | Show interfaces, routes, public IP |\n | network | checkport | `pos-network-checkport` | Check TCP port connectivity |\n | network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |\n | docker | ps | `pos-docker-ps` | Enhanced container overview |\n | docker | health | `pos-docker-health` | Quick health dashboard (exits 1 if unhealthy) |\n | docker | compose | `pos-docker-compose` | Service manager (ls/up/down/restart/logs/update/config) |\n | media | mp3 | `pos-media-mp3` | Download audio as MP3 |\n | media | mp4 | `pos-media-mp4` | Download video with format select |\n | system | firewall | `pos-system-firewall` | Interactive UFW management |\n | ssh | load-keys | `pos-ssh-load-keys` | Load SSH keys into agent |\n | vbox | create | `pos-vbox create` | Create disposable VM (asks \"Enter now?\") |\n | vbox | enter | `pos-vbox enter` | Start and exec into container |\n | vbox | ls | `pos-vbox ls` | List vbox-managed containers only (label-filtered) |\n | vbox | start/stop/rm | `pos-vbox start/stop/rm` | Lifecycle management |\n \n ### Legacy Wrappers\n \n These forward to `pos` transparently: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.\n \n ### pos vbox Details\n \n `pos-vbox` manages disposable Docker containers as lightweight VMs:\n \n - **Container labeling:** All created containers get `linux_post_install.vbox=true` label\n - **`ls` filtering:** `docker ps --filter label=linux_post_install.vbox=true` — only shows vbox-managed containers\n - **Post-create prompt:** After `create`, asks \"Enter now? [Y/n]\" using `confirm` helper\n - **Working dir detection:** `enter` auto-detects bind mount path from container labels\n - **Custom dirs:** `--dir ` or `--dir .` for current directory\n \n ---\n \n ## 5. Shared Library — `lib/common.sh`\n \n Sourced by most scripts. Provides:\n \n | Function | Purpose |\n |----------|---------|\n | `log \"msg\"` | Green `[+]` status message |\n | `warn \"msg\"` | Yellow `[!]` warning |\n | `err \"msg\"` | Red `ERROR:` + exit 1 |\n | `ok \"msg\"` | Green `OK` prefix |\n | `section \"title\"` | Cyan-bordered section header |\n | `step N T \"msg\"` | Numbered step header (e.g., `[1/4] Installing`) |\n | `run cmd` | Executes command, respects `$DRY_RUN` |\n | `spawn \"msg\" cmd` | Runs with animated braille spinner, elapsed time, OK/FAIL status |\n | `timer_start` / `timer_stop` | Elapsed time tracking |\n | `confirm \"prompt\" [default]` | y/N or Y/n prompt |\n \n **Auto-detects TTY** — disables colors when piped.\n \n **Source pattern:**\n ```bash\n source \"$(dirname \"$0\")/../lib/common.sh\"\n ```\n \n **Scripts that do NOT source common.sh** (self-contained): `bin/pos`, `pos-network-ip`, `pos-network-checkport`, `pos-network-scan`, `pos-media-mp3`, `pos-media-mp4`, `pos-ssh-load-keys`, `pos-system-firewall`.\n \n ---\n \n ## 6. Docker Compose / ScaleTail\n \n ### Architecture\n \n ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar pattern (`network_mode: service:tailscale`). Each service gets a `tail-xxxxx.ts.net` URL with optional automatic HTTPS.\n \n ```\n /usr/local/share/linux_post_install/scale-tail/ # Templates (git repo)\n └── services//\n ├── compose.yaml\n └── .env\n \n ~/.config/linux_post_install/compose.env # Global defaults (TS_AUTHKEY, TZ, DNS_SERVER, SERVICES_BASE)\n \n /srv// # Active deployments (default base)\n ├── compose.yaml # From template (refreshed on update)\n ├── .env # User config (preserved across updates)\n ├── config/\n └── data/\n ```\n \n ### Key Commands\n \n | Command | Description |\n |---------|-------------|\n | `pos docker compose ls` | List all available ScaleTail services |\n | `pos docker compose up ` | Deploy service to SERVICES_BASE |\n | `pos docker compose down ` | Stop a deployed service |\n | `pos docker compose restart ` | Restart a service |\n | `pos docker compose logs [-f]` | View/follow logs |\n | `pos docker compose update` | Pull latest templates, refresh compose.yaml (preserves .env) |\n | `pos docker compose config set K=V` | Set global config value |\n | `pos docker compose config show` | Display current config |\n \n ### Global Config Keys\n \n - `TS_AUTHKEY` — Tailscale auth key (required)\n - `TZ` — Timezone\n - `DNS_SERVER` — Custom DNS\n - `SERVICES_BASE` — Deployment root (default: `/srv`)\n \n ---\n \n ## 7. Optional Apps (`apps/`)\n \n ### How They Work\n \n - `apps/install.sh` auto-discovers all `apps//*.sh` files (excluding itself)\n - Three modes: interactive (default), `--all`, or specific app names as arguments\n - Interactive TUI groups apps by category with section headers\n - Each app script is standalone, idempotent, sources `lib/common.sh`\n \n ### Installation Methods\n \n | Method | Apps |\n |--------|------|\n | `apt install` | btop, obs, vlc, vnc-viewer, qemu |\n | APT repo (GPG + repo) | brave, vscode |\n | Official `curl \\| sh` | docker, tailscale, netbird, zerotier, opencode |\n | AppImage | affine |\n | GitHub release binary | scrcpy |\n | Flatpak | localsend |\n | .deb package | termius |\n \n ### Adding a New App\n \n 1. Create `apps/.sh` following the template in DEV.md\n 2. It auto-appears in the interactive picker — no registration needed\n \n ---\n \n ## 8. Systemd Services\n \n | Service | File | Purpose |\n |---------|------|---------|\n | `ssh-agent.service` | `systemd/ssh-agent.service` | System-wide SSH agent, socket at `/run/ssh-agent/socket` |\n | `autostart.service` | `systemd/autostart.service` | Runs `autostart.sh` on boot |\n \n All `.service` files in `systemd/` are automatically copied to `/etc/systemd/system/` and enabled by `postinstall.sh`.\n \n ---\n \n ## 9. Configuration Files\n \n ### Gitignored Secrets\n \n - `config/rclone.conf` — rclone remote config (OAuth tokens)\n - `config/authorized_keys` — SSH public keys\n \n ### Runtime Config\n \n - `~/.config/linux_post_install/compose.env` — Docker Compose global defaults\n - `~/.bashrc` — Modified by postinstall (PATH, bash completion)\n \n ---\n \n ## 10. Coding Conventions\n \n ### Script Standards\n \n - **Shebang:** `#!/usr/bin/env bash`\n - **Strict mode:** `set -euo pipefail`\n - **Help:** Every script accepts `-h`/`--help` via `case` pattern\n - **Idempotency:** Check existence before creating/modifying\n - **Exit codes:** 0 = success, 1 = error\n \n ### Naming Conventions\n \n - `pos--` — canonical tool names\n - `wr-*` — legacy wrappers\n - `apps//.sh` — optional app installers\n - Hyphens for word separation, lowercase always\n \n ### Error Handling\n \n - `command -v &>/dev/null` to check tool availability\n - `set -euo pipefail` for fail-fast\n - `err()` for fatal errors, `warn()` for non-fatal\n - Confirmation prompts for destructive actions\n \n ### Security\n \n - Never hardcode secrets in scripts\n - Use `chmod 600` for sensitive files\n - Validate user input before shell commands\n - Use `sudo` only where necessary\n \n ---\n \n ## 11. Development Workflow\n \n ### Adding a New App\n \n 1. Create `apps//.sh` following the template in DEV.md\n 2. It auto-appears in the interactive picker — no registration needed\n \n ### Adding a New Tool\n \n 1. Create `bin/pos--` following conventions\n 2. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)\n 3. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets)\n 4. Update `README.md`\n 5. Test: `bash -n bin/your-tool && shellcheck bin/your-tool`\n \n ### Testing\n \n ```bash\n # Syntax check all scripts\n for f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do\n bash -n \"$f\" || echo \"FAIL: $f\"\n done\n \n # ShellCheck linting\n shellcheck bin/my-script\n \n # Test in Docker\n docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash\n # inside: cd /repo && ./install.sh\n \n # Test apps interactively\n ./apps/install.sh --all\n ./apps/install.sh docker vscode\n ```\n \n ### Commit Conventions\n \n Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`\n \n ---\n \n ## 12. Key File Quick Reference\n \n | File | Lines | Purpose |\n |------|-------|---------|\n-| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n-| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n-| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n+| `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n+| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n+| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n | `lib/common.sh` | 121 | Shared library |\n-| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n-| `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n+| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n+| `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n | `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n | `bin/pos-docker-ps` | 127 | Enhanced container overview |\n-| `bin/pos-docker-health` | ~90 | Quick health dashboard |\n-| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n+| `bin/pos-docker-health` | 109 | Quick health dashboard |\n+| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n | `completions/pos.bash` | 118 | Dynamic bash completion |\n-| `apps/install.sh` | 99 | App picker/orchestrator |\n+| `apps/install.sh` | 143 | App picker/orchestrator |\n \n ---\n \n ## 13. Common Tasks for Agents\n \n | Task | Where to Edit |\n |------|---------------|\n | Add a new CLI tool | Create `bin/pos--`, add deps in `preinstall.sh` |\n | Add a new app installer | Create `apps/.sh` (auto-discovered) |\n | Add a systemd service | Create `systemd/.service` (auto-installed by postinstall) |\n | Modify package list | Edit `PACKAGES` array in `preinstall.sh` |\n | Change PATH or bash config | Edit `postinstall.sh` |\n | Modify fail2ban config | Edit jail.local section in `postinstall.sh` |\n | Add bash completion | Edit `completions/pos.bash` |\n | Modify Docker Compose logic | Edit `bin/pos-docker-compose` |\n | Modify Docker health check | Edit `bin/pos-docker-health` |\n | Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |\n | Modify pos logging | Edit log setup in `bin/pos` |\n | Modify install phases/flags | Edit arg parsing in `install.sh` |\n | Update documentation | Edit `README.md` and/or `DEV.md` |\n | Add a secret config file | Add to `config/`, update `.gitignore`, add copy logic in `postinstall.sh` |\n", + "additions": 11, + "deletions": 8, + "status": "modified" + } + ] + }, + "id": "msg_faf63442b001yPzbLCG1YN0HxR", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "type": "text", + "text": "yes", + "id": "prt_faf634432001iBYZNtbk9Q6bV0", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63442b001yPzbLCG1YN0HxR" + } + ] + }, + { + "info": { + "parentID": "msg_faf63442b001yPzbLCG1YN0HxR", + "role": "assistant", + "mode": "build", + "agent": "build", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 34424, + "input": 32175, + "output": 112, + "reasoning": 1753, + "cache": { + "write": 0, + "read": 384 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353946188, + "completed": 1785353963864 + }, + "finish": "tool-calls", + "id": "msg_faf63444c002JNygBLYMoAC6ny", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-start", + "id": "prt_faf634cac00158rQc3s8Zp3WFJ", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63444c002JNygBLYMoAC6ny" + }, + { + "type": "reasoning", + "text": "The user approved my plan. Now I'm in build mode and can make file changes. Let me update the AGENT_Context_Project.md with the corrections I outlined.\n\nLet me carefully identify all the changes I need to make:\n\n1. Fix line counts in section 12 (Key File Quick Reference)\n2. Add `config/authorized_keys` to directory tree in section 2\n3. Add `bin/autostart.sh` to directory tree in section 2\n4. Add `bin/autostart.sh` to quick reference in section 12\n5. Add `config/authorized_keys` to gitignored secrets section 9\n6. Fix line counts for install.sh, postinstall.sh, bin/pos, pos-docker-compose, pos-docker-health, pos-vbox, apps/install.sh\n\nLet me also check if there are other minor things I missed. Let me re-read the context doc more carefully.\n\nLooking at the directory tree section (lines 25-92):\n- Missing `config/authorized_keys` - it's at the bottom as a dir entry, but it shows `├── .gitignore` then `└── .gitmodules`. The config directory itself isn't listed at all! Actually wait, looking more carefully - the full directory tree in section 2 does NOT include `config/` directory. But in the actual repo, it exists at `config/authorized_keys`.\n\nWait, looking at the actual repo structure from the exploration:\n```\nconfig/\n └── authorized_keys # SSH public keys (1 line, gitignored)\n```\n\nBut in the AGENT doc section 2 tree, `config/` is not listed. It IS referenced in section 9 about gitignored secrets though.\n\nLet me re-read section 2 more carefully...\n\nLines 25-92 show the directory tree. I see no `config/` directory listed. I should add it.\n\nAlso, `bin/autostart.sh` is not in the directory tree. Let me check... No, `autostart.sh` is not listed in the tree. The tree shows legacy wrappers (wr-*, mp3, mp4, vbox, ssh-load-all) but not autostart.sh.\n\nWait, actually looking again - line 47 says `│ ├── autostart.sh # Boot-time script (via systemd)`. It IS there. Let me re-check...\n\nLine 47: `│ ├── autostart.sh # Boot-time script (via systemd)` - yes it's there.\n\nSo autostart.sh IS in the tree already. Let me look more carefully at what's actually missing.\n\nLooking at the config directory - I see the tree doesn't have a `config/` entry. In the actual repo:\n```\nconfig/\n └── authorized_keys # SSH public keys (1 line, gitignored)\n```\n\nThis should be added between `completions/` and `compose/` in the tree.\n\nNow for section 9 (Configuration Files) - line 305-313:\n```\n### Gitignored Secrets\n\n- `config/rclone.conf` — rclone remote config (OAuth tokens)\n- `config/authorized_keys` — SSH public keys\n```\n\nThis is already correct! Both are listed.\n\nBut wait, the directory tree doesn't have `config/` at all. Let me add it.\n\nNow let me also look at the actual config directory. From the exploration:\n- `config/authorized_keys` - exists, has 1 line, is gitignored\n\nBut the AGENT doc tree doesn't show `config/` directory at all. Let me add it.\n\nLet me now look at all the line counts I need to fix:\n\nSection 12 (lines 390-406):\n- `install.sh` | ~120 → 144\n- `postinstall.sh` | ~65 → 90\n- `bin/pos` | ~130 → 145\n- `bin/pos-docker-compose` | 317 → 363\n- `bin/pos-docker-health` | ~90 → 109\n- `bin/pos-vbox` | ~160 → 156\n- `apps/install.sh` | 99 → 143\n\nAlso, I should add `autostart.sh` to the quick reference table if it's not there.\n\nLet me check... the quick reference table lines 392-406:\n```\n| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n| `lib/common.sh` | 121 | Shared library |\n| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n| `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n| `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n| `bin/pos-docker-ps` | 127 | Enhanced container overview |\n| `bin/pos-docker-health` | ~90 | Quick health dashboard |\n| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n| `completions/pos.bash` | 118 | Dynamic bash completion |\n| `apps/install.sh` | 99 | App picker/orchestrator |\n```\n\nMissing from this table: `autostart.sh` (14 lines). I could add it, but it's minor. Let me check if it's worth adding... It's only 14 lines, so it's a small thing. I'll add it for completeness.\n\nAlso missing: `bin/pos-network-scan` (270 lines), `bin/pos-docker-compose` (listed), but really the key ones are those listed. The quick reference is already a curated list of the most important files, not every file. I won't add unnecessary entries.\n\nLet me now also think about whether I should add `bin/autostart.sh` to the quick reference. It's only 14 lines and is a simple boot-time script. I'll skip it since the table is for \"key\" files and autostart.sh is minor.\n\nOK, let me start making the edits. I'll need to:\n\n1. Add `config/` directory to section 2 directory tree\n2. Fix line counts in section 12\n\nLet me make the edits now.\n\nActually, let me also double-check the line count for `pos-docker-compose`. The exploration says 363 lines but let me verify.\n\nWait, I already read all the files through the exploration agent. Let me use the actual `wc -l` command to verify some of these.\n\nActually, I should just make the edits based on what the exploration agent found. Let me proceed.\n\nFor the directory tree in section 2, I need to add `config/` directory. Currently the tree has:\n```\n│\n├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── compose/\n│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n│\n├── systemd/\n│ ├── autostart.service # Runs autostart.sh on boot\n│ └── ssh-agent.service # System-wide SSH agent socket\n│\n├── README.md # User-facing documentation\n├── DEV.md # Developer guide\n├── .gitignore # Excludes secrets, Python artifacts, OS files\n└── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n```\n\nI should add `config/` between `completions/` and `compose/`:\n```\n│\n├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── config/\n│ └── authorized_keys # SSH public keys (gitignored)\n│\n├── compose/\n│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n```\n\nNow let me make both edits.", + "time": { + "start": 1785353949409, + "end": 1785353960884 + }, + "id": "prt_faf6350e10011CXqprzTThP9TL", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63444c002JNygBLYMoAC6ny" + }, + { + "type": "tool", + "tool": "bash", + "callID": "call_00_g09LMuwq53MjpZOmnhbw3027", + "state": { + "status": "completed", + "input": { + "command": "wc -l install.sh preinstall.sh postinstall.sh lib/common.sh bin/pos bin/pos-docker-compose bin/pos-docker-ps bin/pos-docker-health bin/pos-vbox bin/pos-network-scan bin/pos-system-firewall bin/autostart.sh apps/install.sh completions/pos.bash" + }, + "output": " 144 install.sh\n 52 preinstall.sh\n 90 postinstall.sh\n 121 lib/common.sh\n 145 bin/pos\n 363 bin/pos-docker-compose\n 127 bin/pos-docker-ps\n 109 bin/pos-docker-health\n 156 bin/pos-vbox\n 270 bin/pos-network-scan\n 284 bin/pos-system-firewall\n 14 bin/autostart.sh\n 143 apps/install.sh\n 118 completions/pos.bash\n 2136 total\n", + "metadata": { + "output": " 144 install.sh\n 52 preinstall.sh\n 90 postinstall.sh\n 121 lib/common.sh\n 145 bin/pos\n 363 bin/pos-docker-compose\n 127 bin/pos-docker-ps\n 109 bin/pos-docker-health\n 156 bin/pos-vbox\n 270 bin/pos-network-scan\n 284 bin/pos-system-firewall\n 14 bin/autostart.sh\n 143 apps/install.sh\n 118 completions/pos.bash\n 2136 total\n", + "exit": 0, + "truncated": false + }, + "title": "wc -l install.sh preinstall.sh postinstall.sh lib/common.sh bin/pos bin/pos-docker-compose bin/pos-docker-ps bin/pos-docker-health bin/pos-vbox bin/pos-network-scan bin/pos-system-firewall bin/autostart.sh apps/install.sh completions/pos.bash", + "time": { + "start": 1785353961902, + "end": 1785353961917 + } + }, + "id": "prt_faf637dbc001JbKMRM6INEm6JR", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63444c002JNygBLYMoAC6ny" + }, + { + "reason": "tool-calls", + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-finish", + "tokens": { + "total": 34424, + "input": 32175, + "output": 112, + "reasoning": 1753, + "cache": { + "write": 0, + "read": 384 + } + }, + "cost": 0, + "id": "prt_faf6384fb001v8fYYBcFpxcNZS", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63444c002JNygBLYMoAC6ny" + } + ] + }, + { + "info": { + "parentID": "msg_faf63442b001yPzbLCG1YN0HxR", + "role": "assistant", + "mode": "build", + "agent": "build", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 34792, + "input": 246, + "output": 192, + "reasoning": 50, + "cache": { + "write": 0, + "read": 34304 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353963879, + "completed": 1785353970397 + }, + "finish": "tool-calls", + "id": "msg_faf63896700117dc1q2SKq5oSQ", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "type": "step-start", + "id": "prt_faf6391cf001v81tW2N863L2QH", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63896700117dc1q2SKq5oSQ" + }, + { + "type": "reasoning", + "text": "Good, the line counts match what the exploration agent found. Now let me make the edits:\n\n1. Add `config/` to the directory tree in section 2\n2. Fix line counts in section 12\n\nLet me start editing.", + "time": { + "start": 1785353966557, + "end": 1785353967018 + }, + "id": "prt_faf6393dd001VkXmsWYsnmW6os", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63896700117dc1q2SKq5oSQ" + }, + { + "type": "text", + "text": "Verified exact line counts. Now making the updates.", + "time": { + "start": 1785353967022, + "end": 1785353967915 + }, + "id": "prt_faf6395ae001BnlJe7shO8oE5B", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63896700117dc1q2SKq5oSQ" + }, + { + "type": "tool", + "tool": "edit", + "callID": "call_00_XW339mAWDqYTOHYWrH550262", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "oldString": "├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── compose/", + "newString": "├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── config/\n│ └── authorized_keys # SSH public keys (gitignored)\n│\n├── compose/" + }, + "output": "Edit applied successfully.", + "metadata": { + "diagnostics": {}, + "diff": "Index: /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n===================================================================\n--- /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n+++ /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n@@ -77,8 +77,11 @@\n │\n ├── completions/\n │ └── pos.bash # Bash tab-completion for the pos CLI\n │\n+├── config/\n+│ └── authorized_keys # SSH public keys (gitignored)\n+│\n ├── compose/\n │ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n │\n ├── systemd/\n", + "filediff": { + "file": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "patch": "Index: /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n===================================================================\n--- /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n+++ /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n@@ -77,8 +77,11 @@\n │\n ├── completions/\n │ └── pos.bash # Bash tab-completion for the pos CLI\n │\n+├── config/\n+│ └── authorized_keys # SSH public keys (gitignored)\n+│\n ├── compose/\n │ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n │\n ├── systemd/\n", + "additions": 3, + "deletions": 0 + }, + "truncated": false + }, + "title": "AGENT_Context_Project.md", + "time": { + "start": 1785353968222, + "end": 1785353968232 + } + }, + "id": "prt_faf63963d0015DufydPfeTVWMk", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63896700117dc1q2SKq5oSQ" + }, + { + "reason": "tool-calls", + "snapshot": "f5b6b938032c6af89375f4cfc3adc1da3c76fc50", + "type": "step-finish", + "tokens": { + "total": 34792, + "input": 246, + "output": 192, + "reasoning": 50, + "cache": { + "write": 0, + "read": 34304 + } + }, + "cost": 0, + "id": "prt_faf639f84001umm6VQoSyvgvzJ", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63896700117dc1q2SKq5oSQ" + }, + { + "type": "patch", + "hash": "7a6df0dc98daf9e677885b268d8b5d09c8399541", + "files": [ + "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md" + ], + "id": "prt_faf63a2d7001sRonekB5bIgpjT", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63896700117dc1q2SKq5oSQ" + } + ] + }, + { + "info": { + "parentID": "msg_faf63442b001yPzbLCG1YN0HxR", + "role": "assistant", + "mode": "build", + "agent": "build", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 35395, + "input": 119, + "output": 574, + "reasoning": 14, + "cache": { + "write": 0, + "read": 34688 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353970408, + "completed": 1785353978457 + }, + "finish": "tool-calls", + "id": "msg_faf63a2e8002Su56bYN1Hm55EC", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "f5b6b938032c6af89375f4cfc3adc1da3c76fc50", + "type": "step-start", + "id": "prt_faf63aba2001DTBRqoOvU7Xca3", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63a2e8002Su56bYN1Hm55EC" + }, + { + "type": "reasoning", + "text": "Good, now let me fix the line counts in section 12.", + "time": { + "start": 1785353973142, + "end": 1785353973450 + }, + "id": "prt_faf63ad960014OShbmV0zNiSe1", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63a2e8002Su56bYN1Hm55EC" + }, + { + "type": "tool", + "tool": "edit", + "callID": "call_00_Y6EgTkSpJIJJ2HrDw6CU1082", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "oldString": "| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n| `lib/common.sh` | 121 | Shared library |\n| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n| `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n| `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n| `bin/pos-docker-ps` | 127 | Enhanced container overview |\n| `bin/pos-docker-health` | ~90 | Quick health dashboard |\n| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n| `completions/pos.bash` | 118 | Dynamic bash completion |\n| `apps/install.sh` | 99 | App picker/orchestrator |", + "newString": "| `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n| `lib/common.sh` | 121 | Shared library |\n| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n| `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n| `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n| `bin/pos-docker-ps` | 127 | Enhanced container overview |\n| `bin/pos-docker-health` | 109 | Quick health dashboard |\n| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n| `completions/pos.bash` | 118 | Dynamic bash completion |\n| `apps/install.sh` | 143 | App picker/orchestrator |" + }, + "output": "Edit applied successfully.", + "metadata": { + "diagnostics": {}, + "diff": "Index: /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n===================================================================\n--- /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n+++ /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n@@ -393,20 +393,20 @@\n ## 12. Key File Quick Reference\n \n | File | Lines | Purpose |\n |------|-------|---------|\n-| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n-| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n-| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n+| `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n+| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n+| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n | `lib/common.sh` | 121 | Shared library |\n-| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n-| `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n+| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n+| `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n | `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n | `bin/pos-docker-ps` | 127 | Enhanced container overview |\n-| `bin/pos-docker-health` | ~90 | Quick health dashboard |\n-| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n+| `bin/pos-docker-health` | 109 | Quick health dashboard |\n+| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n | `completions/pos.bash` | 118 | Dynamic bash completion |\n-| `apps/install.sh` | 99 | App picker/orchestrator |\n+| `apps/install.sh` | 143 | App picker/orchestrator |\n \n ---\n \n ## 13. Common Tasks for Agents\n", + "filediff": { + "file": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "patch": "Index: /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n===================================================================\n--- /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n+++ /mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\n@@ -393,20 +393,20 @@\n ## 12. Key File Quick Reference\n \n | File | Lines | Purpose |\n |------|-------|---------|\n-| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |\n-| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |\n-| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |\n+| `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n+| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n+| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n | `lib/common.sh` | 121 | Shared library |\n-| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |\n-| `bin/pos-docker-compose` | 317 | Largest script — full compose management |\n+| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n+| `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n | `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n | `bin/pos-docker-ps` | 127 | Enhanced container overview |\n-| `bin/pos-docker-health` | ~90 | Quick health dashboard |\n-| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n+| `bin/pos-docker-health` | 109 | Quick health dashboard |\n+| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n | `completions/pos.bash` | 118 | Dynamic bash completion |\n-| `apps/install.sh` | 99 | App picker/orchestrator |\n+| `apps/install.sh` | 143 | App picker/orchestrator |\n \n ---\n \n ## 13. Common Tasks for Agents\n", + "additions": 8, + "deletions": 8 + }, + "truncated": false + }, + "title": "AGENT_Context_Project.md", + "time": { + "start": 1785353976266, + "end": 1785353976280 + } + }, + "id": "prt_faf63aecf001b1fh26mHJfmzhY", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63a2e8002Su56bYN1Hm55EC" + }, + { + "reason": "tool-calls", + "snapshot": "2bb724bd7a5d20099270ef42cb400feaebbb030b", + "type": "step-finish", + "tokens": { + "total": 35395, + "input": 119, + "output": 574, + "reasoning": 14, + "cache": { + "write": 0, + "read": 34688 + } + }, + "cost": 0, + "id": "prt_faf63be99001DnPsH9CY6T5S92", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63a2e8002Su56bYN1Hm55EC" + }, + { + "type": "patch", + "hash": "f5b6b938032c6af89375f4cfc3adc1da3c76fc50", + "files": [ + "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md" + ], + "id": "prt_faf63c2540016KOUSNBlcT42Y0", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63a2e8002Su56bYN1Hm55EC" + } + ] + }, + { + "info": { + "parentID": "msg_faf63442b001yPzbLCG1YN0HxR", + "role": "assistant", + "mode": "build", + "agent": "build", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 35621, + "input": 82, + "output": 193, + "reasoning": 18, + "cache": { + "write": 0, + "read": 35328 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353978467, + "completed": 1785353984215 + }, + "finish": "tool-calls", + "id": "msg_faf63c263002Q8WcfJocAUU7C5", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "2bb724bd7a5d20099270ef42cb400feaebbb030b", + "type": "step-start", + "id": "prt_faf63cadc001VTo4Aczy5aG4ff", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63c263002Q8WcfJocAUU7C5" + }, + { + "type": "reasoning", + "text": "Good, both edits applied. Let me verify the final result by reading the modified sections.", + "time": { + "start": 1785353981294, + "end": 1785353981651 + }, + "id": "prt_faf63cd6e001sEJiT0o97fyhoF", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63c263002Q8WcfJocAUU7C5" + }, + { + "type": "text", + "text": "Let me verify the changes are correct.", + "time": { + "start": 1785353981656, + "end": 1785353982474 + }, + "id": "prt_faf63ced8001IpHWgPvAqfjwye", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63c263002Q8WcfJocAUU7C5" + }, + { + "type": "tool", + "tool": "read", + "callID": "call_00_eOj9Pnd24nit7sBnnZCy4126", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "offset": 78, + "limit": 20 + }, + "output": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\nfile\n\n78: ├── completions/\n79: │ └── pos.bash # Bash tab-completion for the pos CLI\n80: │\n81: ├── config/\n82: │ └── authorized_keys # SSH public keys (gitignored)\n83: │\n84: ├── compose/\n85: │ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n86: │\n87: ├── systemd/\n88: │ ├── autostart.service # Runs autostart.sh on boot\n89: │ └── ssh-agent.service # System-wide SSH agent socket\n90: │\n91: ├── README.md # User-facing documentation\n92: ├── DEV.md # Developer guide\n93: ├── .gitignore # Excludes secrets, Python artifacts, OS files\n94: └── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n95: ```\n96: \n97: ---\n\n(Showing lines 78-97 of 429. Use offset=98 to continue.)\n", + "metadata": { + "preview": "├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── config/\n│ └── authorized_keys # SSH public keys (gitignored)\n│\n├── compose/\n│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n│\n├── systemd/\n│ ├── autostart.service # Runs autostart.sh on boot\n│ └── ssh-agent.service # System-wide SSH agent socket\n│\n├── README.md # User-facing documentation\n├── DEV.md # Developer guide\n├── .gitignore # Excludes secrets, Python artifacts, OS files\n└── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n```\n\n---", + "truncated": true, + "loaded": [], + "display": { + "type": "file", + "path": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "text": "├── completions/\n│ └── pos.bash # Bash tab-completion for the pos CLI\n│\n├── config/\n│ └── authorized_keys # SSH public keys (gitignored)\n│\n├── compose/\n│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)\n│\n├── systemd/\n│ ├── autostart.service # Runs autostart.sh on boot\n│ └── ssh-agent.service # System-wide SSH agent socket\n│\n├── README.md # User-facing documentation\n├── DEV.md # Developer guide\n├── .gitignore # Excludes secrets, Python artifacts, OS files\n└── .gitmodules # Submodule: compose/scale-tail → ScaleTail\n```\n\n---", + "lineStart": 78, + "lineEnd": 97, + "totalLines": 429, + "truncated": true + } + }, + "title": "AGENT_Context_Project.md", + "time": { + "start": 1785353982105, + "end": 1785353982488 + } + }, + "id": "prt_faf63cf6c001VRVePMrWUqU63r", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63c263002Q8WcfJocAUU7C5" + }, + { + "type": "tool", + "tool": "read", + "callID": "call_01_e65DLBqAow2whJ1mFahq6791", + "state": { + "status": "completed", + "input": { + "filePath": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "offset": 393, + "limit": 20 + }, + "output": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md\nfile\n\n393: ## 12. Key File Quick Reference\n394: \n395: | File | Lines | Purpose |\n396: |------|-------|---------|\n397: | `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n398: | `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n399: | `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n400: | `lib/common.sh` | 121 | Shared library |\n401: | `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n402: | `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n403: | `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n404: | `bin/pos-docker-ps` | 127 | Enhanced container overview |\n405: | `bin/pos-docker-health` | 109 | Quick health dashboard |\n406: | `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n407: | `completions/pos.bash` | 118 | Dynamic bash completion |\n408: | `apps/install.sh` | 143 | App picker/orchestrator |\n409: \n410: ---\n411: \n412: ## 13. Common Tasks for Agents\n\n(Showing lines 393-412 of 429. Use offset=413 to continue.)\n", + "metadata": { + "preview": "## 12. Key File Quick Reference\n\n| File | Lines | Purpose |\n|------|-------|---------|\n| `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n| `lib/common.sh` | 121 | Shared library |\n| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n| `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n| `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n| `bin/pos-docker-ps` | 127 | Enhanced container overview |\n| `bin/pos-docker-health` | 109 | Quick health dashboard |\n| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n| `completions/pos.bash` | 118 | Dynamic bash completion |\n| `apps/install.sh` | 143 | App picker/orchestrator |\n\n---\n\n## 13. Common Tasks for Agents", + "truncated": true, + "loaded": [], + "display": { + "type": "file", + "path": "/mnt/hdd/ciya_space/work/repo/Linux_post_install/AGENT_Context_Project.md", + "text": "## 12. Key File Quick Reference\n\n| File | Lines | Purpose |\n|------|-------|---------|\n| `install.sh` | 144 | Main orchestrator — 4 phases with CLI flags |\n| `preinstall.sh` | 52 | System packages + yt-dlp + fail2ban |\n| `postinstall.sh` | 90 | fail2ban config, PATH, bash completion, systemd |\n| `lib/common.sh` | 121 | Shared library |\n| `bin/pos` | 145 | CLI dispatcher with smart arg matching + logging |\n| `bin/pos-docker-compose` | 363 | Largest script — full compose management |\n| `bin/pos-system-firewall` | 284 | Interactive UFW manager |\n| `bin/pos-docker-ps` | 127 | Enhanced container overview |\n| `bin/pos-docker-health` | 109 | Quick health dashboard |\n| `bin/pos-vbox` | 156 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |\n| `completions/pos.bash` | 118 | Dynamic bash completion |\n| `apps/install.sh` | 143 | App picker/orchestrator |\n\n---\n\n## 13. Common Tasks for Agents", + "lineStart": 393, + "lineEnd": 412, + "totalLines": 429, + "truncated": true + } + }, + "title": "AGENT_Context_Project.md", + "time": { + "start": 1785353982469, + "end": 1785353982600 + } + }, + "id": "prt_faf63d09d001nMo1b29ObCcY6R", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63c263002Q8WcfJocAUU7C5" + }, + { + "reason": "tool-calls", + "snapshot": "2bb724bd7a5d20099270ef42cb400feaebbb030b", + "type": "step-finish", + "tokens": { + "total": 35621, + "input": 82, + "output": 193, + "reasoning": 18, + "cache": { + "write": 0, + "read": 35328 + } + }, + "cost": 0, + "id": "prt_faf63d5a2001gkDrasLLGzYSq5", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63c263002Q8WcfJocAUU7C5" + } + ] + }, + { + "info": { + "parentID": "msg_faf63442b001yPzbLCG1YN0HxR", + "role": "assistant", + "mode": "build", + "agent": "build", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "total": 36424, + "input": 707, + "output": 121, + "reasoning": 12, + "cache": { + "write": 0, + "read": 35584 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785353984225, + "completed": 1785353989842 + }, + "finish": "stop", + "id": "msg_faf63d8e1002ZYKwBnSIMCQ1Bm", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "snapshot": "2bb724bd7a5d20099270ef42cb400feaebbb030b", + "type": "step-start", + "id": "prt_faf63e206001Ovhg7oBv3rkSBp", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63d8e1002ZYKwBnSIMCQ1Bm" + }, + { + "type": "reasoning", + "text": "Both edits look correct. Let me summarize what was done.", + "time": { + "start": 1785353987180, + "end": 1785353987380 + }, + "id": "prt_faf63e46c0017phtNNFCEBXere", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63d8e1002ZYKwBnSIMCQ1Bm" + }, + { + "type": "text", + "text": "Done. Updated AGENT_Context_Project.md with:\n\n- **Line counts fixed**: `install.sh` 144, `postinstall.sh` 90, `bin/pos` 145, `pos-docker-compose` 363, `pos-docker-health` 109, `pos-vbox` 156, `apps/install.sh` 143\n- **Added `config/` directory** to the directory tree with `authorized_keys` entry\n- The doc is now in sync with the actual codebase — it serves as my complete reference for this repo", + "time": { + "start": 1785353987384, + "end": 1785353988129 + }, + "id": "prt_faf63e538001ncSLkwS5W8xY2A", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63d8e1002ZYKwBnSIMCQ1Bm" + }, + { + "reason": "stop", + "snapshot": "2bb724bd7a5d20099270ef42cb400feaebbb030b", + "type": "step-finish", + "tokens": { + "total": 36424, + "input": 707, + "output": 121, + "reasoning": 12, + "cache": { + "write": 0, + "read": 35584 + } + }, + "cost": 0, + "id": "prt_faf63eb57001DvAnk35rKiyBBy", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf63d8e1002ZYKwBnSIMCQ1Bm" + } + ] + }, + { + "info": { + "role": "user", + "time": { + "created": 1785354112403 + }, + "agent": "plan", + "model": { + "providerID": "opencode", + "modelID": "big-pickle" + }, + "summary": { + "diffs": [] + }, + "id": "msg_faf65cd93001z06jegQgBSlsnO", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [ + { + "type": "text", + "text": "also for make easy to you to track later or make editing or thing about project, create flow logic diagram, create folder named logic_digram and insert how many you want , no problem if you create 9999 times diagram files, I want make project docs abstract as possible , and also beside DEV doc that explain rules and how should developing flow be and convention", + "id": "prt_faf65cd9b001sGdWG4Teeg1VhS", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l", + "messageID": "msg_faf65cd93001z06jegQgBSlsnO" + } + ] + }, + { + "info": { + "parentID": "msg_faf65cd93001z06jegQgBSlsnO", + "role": "assistant", + "mode": "plan", + "agent": "plan", + "path": { + "cwd": "/mnt/hdd/ciya_space/work/repo/Linux_post_install", + "root": "/mnt/hdd/ciya_space/work/repo/Linux_post_install" + }, + "cost": 0, + "tokens": { + "input": 0, + "output": 0, + "reasoning": 0, + "cache": { + "read": 0, + "write": 0 + } + }, + "modelID": "big-pickle", + "providerID": "opencode", + "time": { + "created": 1785354112432, + "completed": 1785354151188 + }, + "id": "msg_faf65cdaf0027QIBqB6JAKhq4t", + "sessionID": "ses_0509eda1dffe8QPzn800kHRa9l" + }, + "parts": [] + } + ] +} diff --git a/x64_bin/create_ap b/x64_bin/create_ap new file mode 100755 index 0000000..bcc57bd --- /dev/null +++ b/x64_bin/create_ap @@ -0,0 +1,2044 @@ +#!/usr/bin/env bash + +# general dependencies: +# bash (to run this script) +# util-linux (for getopt) +# procps or procps-ng +# hostapd +# iproute2 +# iw +# iwconfig (you only need this if 'iw' can not recognize your adapter) +# haveged (optional) + +# dependencies for 'nat' or 'none' Internet sharing method +# dnsmasq +# iptables + +VERSION=0.4.9 +PROGNAME="$(basename $0)" + +# make sure that all command outputs are in english +# so we can parse them correctly +export LC_ALL=C + +# all new files and directories must be readable only by root. +# in special cases we must use chmod to give any other permissions. +SCRIPT_UMASK=0077 +umask $SCRIPT_UMASK + +usage() { + echo "Usage: "$PROGNAME" [options] [] [ []]" + echo + echo "Options:" + echo " -h, --help Show this help" + echo " --version Print version number" + echo " -c Channel number (default: 1 or fallback to currently connected channel)" + echo " -w Use 1 for WPA, use 2 for WPA2, use 1+2 for both (default: 2)" + echo " -n Disable Internet sharing (if you use this, don't pass" + echo " the argument)" + echo " -m Method for Internet sharing." + echo " Use: 'nat' for NAT (default)" + echo " 'bridge' for bridging" + echo " 'none' for no Internet sharing (equivalent to -n)" + echo " --psk Use 64 hex digits pre-shared-key instead of passphrase" + echo " --hidden Make the Access Point hidden (do not broadcast the SSID)" + echo " --mac-filter Enable MAC address filtering" + echo " --mac-filter-accept Location of MAC address filter list (defaults to /etc/hostapd/hostapd.accept)" + echo " --redirect-to-localhost If -n is set, redirect every web request to localhost (useful for public information networks)" + echo " --hostapd-debug With level between 1 and 2, passes arguments -d or -dd to hostapd for debugging." + echo " --hostapd-timestamps Include timestamps in hostapd debug messages." + echo " --isolate-clients Disable communication between clients" + echo " --ieee80211n Enable IEEE 802.11n (HT)" + echo " --ieee80211ac Enable IEEE 802.11ac (VHT)" + echo " --ieee80211ax Enable IEEE 802.11ax (VHT)" + echo " --ht_capab HT capabilities (default: [HT40+])" + echo " --vht_capab VHT capabilities" + echo " --country Set two-letter country code for regularity (example: US)" + echo " --freq-band Set frequency band. Valid inputs: 2.4, 5 (default: Use 5GHz if the interface supports it)" + echo " --driver Choose your WiFi adapter driver (default: nl80211)" + echo " --no-virt Do not create virtual interface" + echo " --no-haveged Do not run 'haveged' automatically when needed" + echo " --fix-unmanaged If NetworkManager shows your interface as unmanaged after you" + echo " close create_ap, then use this option to switch your interface" + echo " back to managed" + echo " --mac Set MAC address" + echo " --dhcp-dns Set DNS returned by DHCP" + echo " --dhcp-hosts Add list of dnsmasq.conf 'dhcp-host=' values" + echo " If ETC_HOSTS=1, it will use the ip addresses for the named hosts in that /etc/hosts." + echo " Othwise, the following syntax would work --dhcp-hosts \"host1,192.168.12.2 host2,192.168.12.3\"" + echo " See https://github.com/imp/dnsmasq/blob/770bce967cfc9967273d0acfb3ea018fb7b17522/dnsmasq.conf.example#L238" + echo " for other valid dnsmasq dhcp-host parameters." + echo " --daemon Run create_ap in the background" + echo " --pidfile Save daemon PID to file" + echo " --logfile Save daemon messages to file" + echo " --dns-logfile Log DNS queries to file" + echo " --stop Send stop command to an already running create_ap. For an " + echo " you can put the PID of create_ap or the WiFi interface. You can" + echo " get them with --list-running" + echo " --list-running Show the create_ap processes that are already running" + echo " --list-clients List the clients connected to create_ap instance associated with ." + echo " For an you can put the PID of create_ap or the WiFi interface." + echo " If virtual WiFi interface was created, then use that one." + echo " You can get them with --list-running" + echo " --mkconfig Store configs in conf_file" + echo " --config Load configs from conf_file" + echo + echo "Non-Bridging Options:" + echo " --no-dns Disable dnsmasq DNS server" + echo " --no-dnsmasq Disable dnsmasq server completely" + echo " -g IPv4 Gateway for the Access Point (default: 192.168.12.1)" + echo " -d DNS server will take into account /etc/hosts" + echo " -e DNS server will take into account additional hosts file" + echo + echo "Useful informations:" + echo " * If you're not using the --no-virt option, then you can create an AP with the same" + echo " interface you are getting your Internet connection." + echo " * You can pass your SSID and password through pipe or through arguments (see examples)." + echo " * On bridge method if the is not a bridge interface, then" + echo " a bridge interface is created automatically." + echo + echo "Examples:" + echo " "$PROGNAME" wlan0 eth0 MyAccessPoint MyPassPhrase" + echo " echo -e 'MyAccessPoint\nMyPassPhrase' | "$PROGNAME" wlan0 eth0" + echo " "$PROGNAME" wlan0 eth0 MyAccessPoint" + echo " echo 'MyAccessPoint' | "$PROGNAME" wlan0 eth0" + echo " "$PROGNAME" wlan0 wlan0 MyAccessPoint MyPassPhrase" + echo " "$PROGNAME" -n wlan0 MyAccessPoint MyPassPhrase" + echo " "$PROGNAME" -m bridge wlan0 eth0 MyAccessPoint MyPassPhrase" + echo " "$PROGNAME" -m bridge wlan0 br0 MyAccessPoint MyPassPhrase" + echo " "$PROGNAME" --driver rtl871xdrv wlan0 eth0 MyAccessPoint MyPassPhrase" + echo " "$PROGNAME" --daemon wlan0 eth0 MyAccessPoint MyPassPhrase" + echo " "$PROGNAME" --stop wlan0" +} + +# Busybox polyfills +if cp --help 2>&1 | grep -q -- --no-clobber; then + cp_n() { + cp -n "$@" + } +else + cp_n() { + yes n | cp -i "$@" + } +fi + +# on success it echos a non-zero unused FD +# on error it echos 0 +get_avail_fd() { + local x + for x in $(seq 1 $(ulimit -n)); do + if [[ ! -a "/proc/$BASHPID/fd/$x" ]]; then + echo $x + return + fi + done + echo 0 +} + +# lock file for the mutex counter +COUNTER_LOCK_FILE=/tmp/create_ap.$$.lock + +cleanup_lock() { + rm -f $COUNTER_LOCK_FILE +} + +init_lock() { + local LOCK_FILE=/tmp/create_ap.all.lock + + # we initialize only once + [[ $LOCK_FD -ne 0 ]] && return 0 + + LOCK_FD=$(get_avail_fd) + [[ $LOCK_FD -eq 0 ]] && return 1 + + # open/create lock file with write access for all users + # otherwise normal users will not be able to use it. + # to avoid race conditions on creation, we need to + # use umask to set the permissions. + umask 0555 + eval "exec $LOCK_FD>$LOCK_FILE" > /dev/null 2>&1 || return 1 + umask $SCRIPT_UMASK + + # there is a case where lock file was created from a normal + # user. change the owner to root as soon as we can. + [[ $(id -u) -eq 0 ]] && chown 0:0 $LOCK_FILE + + # create mutex counter lock file + echo 0 > $COUNTER_LOCK_FILE + + return $? +} + +# recursive mutex lock for all create_ap processes +mutex_lock() { + local counter_mutex_fd + local counter + + # lock local mutex and read counter + counter_mutex_fd=$(get_avail_fd) + if [[ $counter_mutex_fd -ne 0 ]]; then + eval "exec $counter_mutex_fd<>$COUNTER_LOCK_FILE" + flock $counter_mutex_fd + read -u $counter_mutex_fd counter + else + echo "Failed to lock mutex counter" >&2 + return 1 + fi + + # lock global mutex and increase counter + [[ $counter -eq 0 ]] && flock $LOCK_FD + counter=$(( $counter + 1 )) + + # write counter and unlock local mutex + echo $counter > /proc/$BASHPID/fd/$counter_mutex_fd + eval "exec ${counter_mutex_fd}<&-" + return 0 +} + +# recursive mutex unlock for all create_ap processes +mutex_unlock() { + local counter_mutex_fd + local counter + + # lock local mutex and read counter + counter_mutex_fd=$(get_avail_fd) + if [[ $counter_mutex_fd -ne 0 ]]; then + eval "exec $counter_mutex_fd<>$COUNTER_LOCK_FILE" + flock $counter_mutex_fd + read -u $counter_mutex_fd counter + else + echo "Failed to lock mutex counter" >&2 + return 1 + fi + + # decrease counter and unlock global mutex + if [[ $counter -gt 0 ]]; then + counter=$(( $counter - 1 )) + [[ $counter -eq 0 ]] && flock -u $LOCK_FD + fi + + # write counter and unlock local mutex + echo $counter > /proc/$BASHPID/fd/$counter_mutex_fd + eval "exec ${counter_mutex_fd}<&-" + return 0 +} + +# it takes 2 arguments +# returns: +# 0 if v1 (1st argument) and v2 (2nd argument) are the same +# 1 if v1 is less than v2 +# 2 if v1 is greater than v2 +version_cmp() { + local V1 V2 VN x + [[ ! $1 =~ ^[0-9]+(\.[0-9]+)*$ ]] && die "Wrong version format!" + [[ ! $2 =~ ^[0-9]+(\.[0-9]+)*$ ]] && die "Wrong version format!" + + V1=( $(echo $1 | tr '.' ' ') ) + V2=( $(echo $2 | tr '.' ' ') ) + VN=${#V1[@]} + [[ $VN -lt ${#V2[@]} ]] && VN=${#V2[@]} + + for ((x = 0; x < $VN; x++)); do + [[ ${V1[x]} -lt ${V2[x]} ]] && return 1 + [[ ${V1[x]} -gt ${V2[x]} ]] && return 2 + done + + return 0 +} + +USE_IWCONFIG=0 + +is_interface() { + [[ -z "$1" ]] && return 1 + [[ -d "/sys/class/net/${1}" ]] +} + +is_wifi_interface() { + which iw > /dev/null 2>&1 && iw dev $1 info > /dev/null 2>&1 && return 0 + if which iwconfig > /dev/null 2>&1 && iwconfig $1 > /dev/null 2>&1; then + USE_IWCONFIG=1 + return 0 + fi + return 1 +} + +is_bridge_interface() { + [[ -z "$1" ]] && return 1 + [[ -d "/sys/class/net/${1}/bridge" ]] +} + +get_phy_device() { + local x + for x in /sys/class/ieee80211/*; do + [[ ! -e "$x" ]] && continue + if [[ "${x##*/}" = "$1" ]]; then + echo $1 + return 0 + elif [[ -e "$x/device/net/$1" ]]; then + echo ${x##*/} + return 0 + elif [[ -e "$x/device/net:$1" ]]; then + echo ${x##*/} + return 0 + fi + done + echo "Failed to get phy interface" >&2 + return 1 +} + +get_adapter_info() { + local PHY + PHY=$(get_phy_device "$1") + [[ $? -ne 0 ]] && return 1 + iw phy $PHY info +} + +get_adapter_kernel_module() { + local MODULE + MODULE=$(readlink -f "/sys/class/net/$1/device/driver/module") + echo ${MODULE##*/} +} + +can_be_sta_and_ap() { + # iwconfig does not provide this information, assume false + [[ $USE_IWCONFIG -eq 1 ]] && return 1 + if [[ "$(get_adapter_kernel_module "$1")" == "brcmfmac" ]]; then + echo "WARN: brmfmac driver doesn't work properly with virtual interfaces and" >&2 + echo " it can cause kernel panic. For this reason we disallow virtual" >&2 + echo " interfaces for your adapter." >&2 + echo " For more info: https://github.com/oblique/create_ap/issues/203" >&2 + return 1 + fi + get_adapter_info "$1" | grep -E '{.* managed.* AP.*}' > /dev/null 2>&1 && return 0 + get_adapter_info "$1" | grep -E '{.* AP.* managed.*}' > /dev/null 2>&1 && return 0 + return 1 +} + +can_be_ap() { + # iwconfig does not provide this information, assume true + [[ $USE_IWCONFIG -eq 1 ]] && return 0 + get_adapter_info "$1" | grep -E '\* AP$' > /dev/null 2>&1 && return 0 + return 1 +} + +can_transmit_to_channel() { + local IFACE CHANNEL_NUM CHANNEL_INFO + IFACE=$1 + CHANNEL_NUM=$2 + + if [[ $USE_IWCONFIG -eq 0 ]]; then + if [[ $FREQ_BAND == 2.4 ]]; then + CHANNEL_INFO=$(get_adapter_info ${IFACE} | grep " 24[0-9][0-9]\(\.0\+\)\? MHz \[${CHANNEL_NUM}\]") + else + CHANNEL_INFO=$(get_adapter_info ${IFACE} | grep " \(49[0-9][0-9]\|5[0-9]\{3\}\)\(\.0\+\)\? MHz \[${CHANNEL_NUM}\]") + fi + [[ -z "${CHANNEL_INFO}" ]] && return 1 + [[ "${CHANNEL_INFO}" == *no\ IR* ]] && return 1 + [[ "${CHANNEL_INFO}" == *disabled* ]] && return 1 + return 0 + else + CHANNEL_NUM=$(printf '%02d' ${CHANNEL_NUM}) + CHANNEL_INFO=$(iwlist ${IFACE} channel | grep -E "Channel[[:blank:]]${CHANNEL_NUM}[[:blank:]]?:") + [[ -z "${CHANNEL_INFO}" ]] && return 1 + return 0 + fi +} + +# taken from iw/util.c +ieee80211_frequency_to_channel() { + local FREQ_MAYBE_FRACTIONAL=$1 + local FREQ=${FREQ_MAYBE_FRACTIONAL%.*} + + if [[ $FREQ -lt 1000 ]]; then + echo 0 + elif [[ $FREQ -eq 2484 ]]; then + echo 14 + elif [[ $FREQ -eq 5935 ]]; then + echo 2 + elif [[ $FREQ -lt 2484 ]]; then + echo $(( ($FREQ - 2407) / 5 )) + elif [[ $FREQ -ge 4910 && $FREQ -le 4980 ]]; then + echo $(( ($FREQ - 4000) / 5 )) + elif [[ $FREQ -lt 5950 ]]; then + echo $(( ($FREQ - 5000) / 5 )) + elif [[ $FREQ -le 45000 ]]; then + echo $(( ($FREQ - 5950) / 5 )) + elif [[ $FREQ -ge 58320 && $FREQ -le 70200 ]]; then + echo $(( ($FREQ - 56160) / 2160 )) + else + echo 0 + fi +} + +is_5ghz_frequency() { + [[ $1 =~ ^(49[0-9]{2})|(5[0-9]{3})(\.0+)?$ ]] +} + +is_wifi_connected() { + if [[ $USE_IWCONFIG -eq 0 ]]; then + iw dev "$1" link 2>&1 | grep -E '^Connected to' > /dev/null 2>&1 && return 0 + else + iwconfig "$1" 2>&1 | grep -E 'Access Point: [0-9a-fA-F]{2}:' > /dev/null 2>&1 && return 0 + fi + return 1 +} + +is_macaddr() { + echo "$1" | grep -E "^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$" > /dev/null 2>&1 +} + +is_unicast_macaddr() { + local x + is_macaddr "$1" || return 1 + x=$(echo "$1" | cut -d: -f1) + x=$(printf '%d' "0x${x}") + [[ $(expr $x % 2) -eq 0 ]] +} + +get_macaddr() { + is_interface "$1" || return + cat "/sys/class/net/${1}/address" +} + +get_mtu() { + is_interface "$1" || return + cat "/sys/class/net/${1}/mtu" +} + +alloc_new_iface() { + local prefix=$1 + local i=0 + + mutex_lock + while :; do + if ! is_interface $prefix$i && [[ ! -f $COMMON_CONFDIR/ifaces/$prefix$i ]]; then + mkdir -p $COMMON_CONFDIR/ifaces + touch $COMMON_CONFDIR/ifaces/$prefix$i + echo $prefix$i + mutex_unlock + return + fi + i=$((i + 1)) + done + mutex_unlock +} + +dealloc_iface() { + rm -f $COMMON_CONFDIR/ifaces/$1 +} + +get_all_macaddrs() { + cat /sys/class/net/*/address +} + +get_new_macaddr() { + local OLDMAC NEWMAC LAST_BYTE i + OLDMAC=$(get_macaddr "$1") + LAST_BYTE=$(printf %d 0x${OLDMAC##*:}) + mutex_lock + for i in {1..255}; do + NEWMAC="${OLDMAC%:*}:$(printf %02x $(( ($LAST_BYTE + $i) % 256 )))" + (get_all_macaddrs | grep "$NEWMAC" > /dev/null 2>&1) || break + done + mutex_unlock + echo $NEWMAC +} + +# start haveged when needed +haveged_watchdog() { + local show_warn=1 + while :; do + if [[ $(cat /proc/sys/kernel/random/entropy_avail) -lt 1000 ]]; then + if ! which haveged > /dev/null 2>&1; then + if [[ $show_warn -eq 1 ]]; then + echo "WARN: Low entropy detected. We recommend you to install \`haveged'" + show_warn=0 + fi + elif ! pidof haveged > /dev/null 2>&1; then + echo "Low entropy detected, starting haveged" + # boost low-entropy + mutex_lock + haveged -w 1024 -p $COMMON_CONFDIR/haveged.pid + mutex_unlock + fi + fi + sleep 2 + done +} + +NETWORKMANAGER_CONF=/etc/NetworkManager/NetworkManager.conf +NM_OLDER_VERSION=1 + +networkmanager_exists() { + local NM_VER + which nmcli > /dev/null 2>&1 || return 1 + NM_VER=$(nmcli -v | grep -m1 -oE '[0-9]+(\.[0-9]+)*\.[0-9]+') + version_cmp $NM_VER 0.9.9 + if [[ $? -eq 1 ]]; then + NM_OLDER_VERSION=1 + else + NM_OLDER_VERSION=0 + fi + return 0 +} + +networkmanager_is_running() { + local NMCLI_OUT + networkmanager_exists || return 1 + if [[ $NM_OLDER_VERSION -eq 1 ]]; then + NMCLI_OUT=$(nmcli -t -f RUNNING nm 2>&1 | grep -E '^running$') + else + NMCLI_OUT=$(nmcli -t -f RUNNING g 2>&1 | grep -E '^running$') + fi + [[ -n "$NMCLI_OUT" ]] +} + +networkmanager_knows_iface() { + # check if the interface $1 is known to NetworkManager + # an interface may exist but may not be known to NetworkManager if it is in a different network namespace than NetworkManager + nmcli -t -f DEVICE d 2>&1 | grep -Fxq "$1" +} + +networkmanager_iface_is_unmanaged() { + is_interface "$1" || return 2 + networkmanager_knows_iface "$1" || return 0 + (nmcli -t -f DEVICE,STATE d 2>&1 | grep -E "^$1:unmanaged$" > /dev/null 2>&1) || return 1 +} + +ADDED_UNMANAGED= + +networkmanager_add_unmanaged() { + local MAC UNMANAGED WAS_EMPTY x + networkmanager_exists || return 1 + + [[ -d ${NETWORKMANAGER_CONF%/*} ]] || mkdir -p ${NETWORKMANAGER_CONF%/*} + [[ -f ${NETWORKMANAGER_CONF} ]] || touch ${NETWORKMANAGER_CONF} + + if [[ $NM_OLDER_VERSION -eq 1 ]]; then + if [[ -z "$2" ]]; then + MAC=$(get_macaddr "$1") + else + MAC="$2" + fi + [[ -z "$MAC" ]] && return 1 + fi + + mutex_lock + UNMANAGED=$(grep -m1 -Eo '^unmanaged-devices=[[:alnum:]:;,?*~=-]*' /etc/NetworkManager/NetworkManager.conf) + + WAS_EMPTY=0 + [[ -z "$UNMANAGED" ]] && WAS_EMPTY=1 + UNMANAGED=$(echo "$UNMANAGED" | sed 's/unmanaged-devices=//' | tr ';,' ' ') + + # if it exists, do nothing + for x in $UNMANAGED; do + if [[ $x == "mac:${MAC}" ]] || + [[ $NM_OLDER_VERSION -eq 0 && $x == "interface-name:${1}" ]]; then + mutex_unlock + return 2 + fi + done + + if [[ $NM_OLDER_VERSION -eq 1 ]]; then + UNMANAGED="${UNMANAGED} mac:${MAC}" + else + UNMANAGED="${UNMANAGED} interface-name:${1}" + fi + + UNMANAGED=$(echo $UNMANAGED | sed -e 's/^ //') + UNMANAGED="${UNMANAGED// /;}" + UNMANAGED="unmanaged-devices=${UNMANAGED}" + + if ! grep -E '^\[keyfile\]' ${NETWORKMANAGER_CONF} > /dev/null 2>&1; then + echo -e "\n\n[keyfile]\n${UNMANAGED}" >> ${NETWORKMANAGER_CONF} + elif [[ $WAS_EMPTY -eq 1 ]]; then + sed -e "s/^\(\[keyfile\].*\)$/\1\n${UNMANAGED}/" -i ${NETWORKMANAGER_CONF} + else + sed -e "s/^unmanaged-devices=.*/${UNMANAGED}/" -i ${NETWORKMANAGER_CONF} + fi + + ADDED_UNMANAGED="${ADDED_UNMANAGED} ${1} " + mutex_unlock + + local nm_pid=$(pidof NetworkManager) + [[ -n "$nm_pid" ]] && kill -HUP $nm_pid + + return 0 +} + +networkmanager_rm_unmanaged() { + local MAC UNMANAGED + networkmanager_exists || return 1 + [[ ! -f ${NETWORKMANAGER_CONF} ]] && return 1 + + if [[ $NM_OLDER_VERSION -eq 1 ]]; then + if [[ -z "$2" ]]; then + MAC=$(get_macaddr "$1") + else + MAC="$2" + fi + [[ -z "$MAC" ]] && return 1 + fi + + mutex_lock + UNMANAGED=$(grep -m1 -Eo '^unmanaged-devices=[[:alnum:]:;,?*~=-]*' /etc/NetworkManager/NetworkManager.conf | sed 's/unmanaged-devices=//' | tr ';,' ' ') + + if [[ -z "$UNMANAGED" ]]; then + mutex_unlock + return 1 + fi + + [[ -n "$MAC" ]] && UNMANAGED=$(echo $UNMANAGED | sed -e "s/mac:${MAC}\( \|$\)//g") + UNMANAGED=$(echo $UNMANAGED | sed -e "s/interface-name:${1}\( \|$\)//g") + UNMANAGED=$(echo $UNMANAGED | sed -e 's/ $//') + + if [[ -z "$UNMANAGED" ]]; then + sed -e "/^unmanaged-devices=.*/d" -i ${NETWORKMANAGER_CONF} + else + UNMANAGED="${UNMANAGED// /;}" + UNMANAGED="unmanaged-devices=${UNMANAGED}" + sed -e "s/^unmanaged-devices=.*/${UNMANAGED}/" -i ${NETWORKMANAGER_CONF} + fi + + ADDED_UNMANAGED="${ADDED_UNMANAGED/ ${1} /}" + mutex_unlock + + local nm_pid=$(pidof NetworkManager) + [[ -n "$nm_pid" ]] && kill -HUP $nm_pid + + return 0 +} + +networkmanager_fix_unmanaged() { + [[ -f ${NETWORKMANAGER_CONF} ]] || return + + mutex_lock + sed -e "/^unmanaged-devices=.*/d" -i ${NETWORKMANAGER_CONF} + mutex_unlock + + local nm_pid=$(pidof NetworkManager) + [[ -n "$nm_pid" ]] && kill -HUP $nm_pid +} + +networkmanager_rm_unmanaged_if_needed() { + [[ $ADDED_UNMANAGED =~ .*\ ${1}\ .* ]] && networkmanager_rm_unmanaged $1 $2 +} + +networkmanager_wait_until_unmanaged() { + local RES + networkmanager_is_running || return 1 + while :; do + networkmanager_iface_is_unmanaged "$1" + RES=$? + [[ $RES -eq 0 ]] && break + [[ $RES -eq 2 ]] && die "Interface '${1}' does not exist. + It's probably renamed by a udev rule." + sleep 1 + done + sleep 2 + return 0 +} + + +CHANNEL=default +GATEWAY=192.168.12.1 +WPA_VERSION=2 +ETC_HOSTS=0 +ADDN_HOSTS= +DHCP_HOSTS= +DHCP_DNS=gateway +NO_DNS=0 +NO_DNSMASQ=0 +DNS_PORT= +HIDDEN=0 +MAC_FILTER=0 +MAC_FILTER_ACCEPT=/etc/hostapd/hostapd.accept +ISOLATE_CLIENTS=0 +SHARE_METHOD=nat +IEEE80211N=0 +IEEE80211AC=0 +IEEE80211AX=0 +HT_CAPAB='[HT40+]' +VHT_CAPAB= +DRIVER=nl80211 +NO_VIRT=0 +COUNTRY= +FREQ_BAND=2.4 +NEW_MACADDR= +DAEMONIZE=0 +DAEMON_PIDFILE= +DAEMON_LOGFILE=/dev/null +DNS_LOGFILE= +NO_HAVEGED=0 +USE_PSK=0 + +HOSTAPD_DEBUG_ARGS= +REDIRECT_TO_LOCALHOST=0 + +CONFIG_OPTS=(CHANNEL GATEWAY WPA_VERSION ETC_HOSTS DHCP_DNS NO_DNS NO_DNSMASQ HIDDEN MAC_FILTER MAC_FILTER_ACCEPT ISOLATE_CLIENTS + SHARE_METHOD IEEE80211N IEEE80211AC IEEE80211AX HT_CAPAB VHT_CAPAB DRIVER NO_VIRT COUNTRY FREQ_BAND + NEW_MACADDR DAEMONIZE DAEMON_PIDFILE DAEMON_LOGFILE DNS_LOGFILE NO_HAVEGED WIFI_IFACE INTERNET_IFACE + SSID PASSPHRASE USE_PSK ADDN_HOSTS DHCP_HOSTS) + +FIX_UNMANAGED=0 +LIST_RUNNING=0 +STOP_ID= +LIST_CLIENTS_ID= + +STORE_CONFIG= +LOAD_CONFIG= + +CONFDIR= +WIFI_IFACE= +VWIFI_IFACE= +INTERNET_IFACE= +BRIDGE_IFACE= +OLD_MACADDR= +IP_ADDRS= +ROUTE_ADDRS= + +HAVEGED_WATCHDOG_PID= + +_cleanup() { + local PID x + + trap "" SIGINT SIGUSR1 SIGUSR2 EXIT + mutex_lock + disown -a + + # kill haveged_watchdog + [[ -n "$HAVEGED_WATCHDOG_PID" ]] && kill $HAVEGED_WATCHDOG_PID + + # kill processes + for x in $CONFDIR/*.pid; do + # even if the $CONFDIR is empty, the for loop will assign + # a value in $x. so we need to check if the value is a file + [[ -f $x ]] && kill -9 $(cat $x) + done + + rm -rf $CONFDIR + + local found=0 + for x in $(list_running_conf); do + if [[ -f $x/nat_internet_iface && $(cat $x/nat_internet_iface) == $INTERNET_IFACE ]]; then + found=1 + break + fi + done + + if [[ $found -eq 0 ]]; then + cp -f $COMMON_CONFDIR/${INTERNET_IFACE}_forwarding \ + /proc/sys/net/ipv4/conf/$INTERNET_IFACE/forwarding + rm -f $COMMON_CONFDIR/${INTERNET_IFACE}_forwarding + fi + + # if we are the last create_ap instance then set back the common values + if ! has_running_instance; then + # kill common processes + for x in $COMMON_CONFDIR/*.pid; do + [[ -f $x ]] && kill -9 $(cat $x) + done + + # set old ip_forward + if [[ -f $COMMON_CONFDIR/ip_forward ]]; then + cp -f $COMMON_CONFDIR/ip_forward /proc/sys/net/ipv4 + rm -f $COMMON_CONFDIR/ip_forward + fi + + # set old bridge-nf-call-iptables + if [[ -f $COMMON_CONFDIR/bridge-nf-call-iptables ]]; then + if [[ -e /proc/sys/net/bridge/bridge-nf-call-iptables ]]; then + cp -f $COMMON_CONFDIR/bridge-nf-call-iptables /proc/sys/net/bridge + fi + rm -f $COMMON_CONFDIR/bridge-nf-call-iptables + fi + + rm -rf $COMMON_CONFDIR + fi + + if [[ "$SHARE_METHOD" != "none" ]]; then + if [[ "$SHARE_METHOD" == "nat" ]]; then + iptables -w -t nat -D POSTROUTING -s ${GATEWAY%.*}.0/24 ! -o ${WIFI_IFACE} -j MASQUERADE + iptables -w -D FORWARD -i ${WIFI_IFACE} -s ${GATEWAY%.*}.0/24 -j ACCEPT + iptables -w -D FORWARD -i ${INTERNET_IFACE} -d ${GATEWAY%.*}.0/24 -j ACCEPT + elif [[ "$SHARE_METHOD" == "bridge" ]]; then + if ! is_bridge_interface $INTERNET_IFACE; then + ip link set dev $BRIDGE_IFACE down + ip link set dev $INTERNET_IFACE down + ip link set dev $INTERNET_IFACE promisc off + ip link set dev $INTERNET_IFACE nomaster + ip link delete $BRIDGE_IFACE type bridge + ip addr flush $INTERNET_IFACE + ip link set dev $INTERNET_IFACE up + dealloc_iface $BRIDGE_IFACE + + for x in "${IP_ADDRS[@]}"; do + x="${x/inet/}" + x="${x/secondary/}" + x="${x/dynamic/}" + x=$(echo $x | sed 's/\([0-9]\)sec/\1/g') + x="${x/${INTERNET_IFACE}/}" + ip addr add $x dev $INTERNET_IFACE + done + + ip route flush dev $INTERNET_IFACE + + for x in "${ROUTE_ADDRS[@]}"; do + [[ -z "$x" ]] && continue + [[ "$x" == default* ]] && continue + ip route add $x dev $INTERNET_IFACE + done + + for x in "${ROUTE_ADDRS[@]}"; do + [[ -z "$x" ]] && continue + [[ "$x" != default* ]] && continue + ip route add $x dev $INTERNET_IFACE + done + + networkmanager_rm_unmanaged_if_needed $INTERNET_IFACE + fi + fi + fi + + if [[ "$SHARE_METHOD" != "bridge" ]]; then + if [[ $NO_DNS -eq 0 ]]; then + iptables -w -D INPUT -p tcp -m tcp --dport $DNS_PORT -j ACCEPT + iptables -w -D INPUT -p udp -m udp --dport $DNS_PORT -j ACCEPT + iptables -w -t nat -D PREROUTING -s ${GATEWAY%.*}.0/24 -d ${GATEWAY} \ + -p tcp -m tcp --dport 53 -j REDIRECT --to-ports $DNS_PORT + iptables -w -t nat -D PREROUTING -s ${GATEWAY%.*}.0/24 -d ${GATEWAY} \ + -p udp -m udp --dport 53 -j REDIRECT --to-ports $DNS_PORT + fi + iptables -w -D INPUT -p udp -m udp --dport 67 -j ACCEPT + fi + + if [[ $NO_VIRT -eq 0 ]]; then + if [[ -n "$VWIFI_IFACE" ]]; then + ip link set down dev ${VWIFI_IFACE} + ip addr flush ${VWIFI_IFACE} + networkmanager_rm_unmanaged_if_needed ${VWIFI_IFACE} ${OLD_MACADDR} + iw dev ${VWIFI_IFACE} del + dealloc_iface $VWIFI_IFACE + fi + else + ip link set down dev ${WIFI_IFACE} + ip addr flush ${WIFI_IFACE} + if [[ -n "$NEW_MACADDR" ]]; then + ip link set dev ${WIFI_IFACE} address ${OLD_MACADDR} + fi + networkmanager_rm_unmanaged_if_needed ${WIFI_IFACE} ${OLD_MACADDR} + fi + + mutex_unlock + cleanup_lock + + if [[ $RUNNING_AS_DAEMON -eq 1 && -n "$DAEMON_PIDFILE" && -f "$DAEMON_PIDFILE" ]]; then + rm $DAEMON_PIDFILE + fi +} + +cleanup() { + echo + echo -n "Doing cleanup.. " + _cleanup > /dev/null 2>&1 + echo "done" +} + +die() { + [[ -n "$1" ]] && echo -e "\nERROR: $1\n" >&2 + # send die signal to the main process + [[ $BASHPID -ne $$ ]] && kill -USR2 $$ + # we don't need to call cleanup because it's traped on EXIT + exit 1 +} + +clean_exit() { + # send clean_exit signal to the main process + [[ $BASHPID -ne $$ ]] && kill -USR1 $$ + # we don't need to call cleanup because it's traped on EXIT + exit 0 +} + +list_running_conf() { + local x + mutex_lock + for x in /tmp/create_ap.*; do + if [[ -f $x/pid && -f $x/wifi_iface && -d /proc/$(cat $x/pid) ]]; then + echo $x + fi + done + mutex_unlock +} + +list_running() { + local IFACE wifi_iface x + mutex_lock + for x in $(list_running_conf); do + IFACE=${x#*.} + IFACE=${IFACE%%.*} + wifi_iface=$(cat $x/wifi_iface) + + if [[ $IFACE == $wifi_iface ]]; then + echo $(cat $x/pid) $IFACE + else + echo $(cat $x/pid) $IFACE '('$(cat $x/wifi_iface)')' + fi + done + mutex_unlock +} + +get_wifi_iface_from_pid() { + list_running | awk '{print $1 " " $NF}' | tr -d '\(\)' | grep -E "^${1} " | cut -d' ' -f2 +} + +get_pid_from_wifi_iface() { + list_running | awk '{print $1 " " $NF}' | tr -d '\(\)' | grep -E " ${1}$" | cut -d' ' -f1 +} + +get_confdir_from_pid() { + local IFACE x + mutex_lock + for x in $(list_running_conf); do + if [[ $(cat $x/pid) == "$1" ]]; then + echo $x + break + fi + done + mutex_unlock +} + +print_client() { + local line ipaddr hostname + local mac="$1" + + if [[ -f $CONFDIR/dnsmasq.leases ]]; then + line=$(grep " $mac " $CONFDIR/dnsmasq.leases | tail -n 1) + ipaddr=$(echo $line | cut -d' ' -f3) + hostname=$(echo "$line" | cut -d' ' -f4) + fi + + [[ -z "$ipaddr" ]] && ipaddr="*" + [[ -z "$hostname" ]] && hostname="*" + + printf "%-20s %-18s %s\n" "$mac" "$ipaddr" "$hostname" +} + +list_clients() { + local wifi_iface pid + + # If PID is given, get the associated wifi iface + if [[ "$1" =~ ^[1-9][0-9]*$ ]]; then + pid="$1" + wifi_iface=$(get_wifi_iface_from_pid "$pid") + [[ -z "$wifi_iface" ]] && die "'$pid' is not the pid of a running $PROGNAME instance." + fi + + [[ -z "$wifi_iface" ]] && wifi_iface="$1" + is_wifi_interface "$wifi_iface" || die "'$wifi_iface' is not a WiFi interface." + + [[ -z "$pid" ]] && pid=$(get_pid_from_wifi_iface "$wifi_iface") + [[ -z "$pid" ]] && die "'$wifi_iface' is not used from $PROGNAME instance.\n\ + Maybe you need to pass the virtual interface instead.\n\ + Use --list-running to find it out." + [[ -z "$CONFDIR" ]] && CONFDIR=$(get_confdir_from_pid "$pid") + + if [[ $USE_IWCONFIG -eq 0 ]]; then + local awk_cmd='($1 ~ /Station$/) {print $2}' + local client_list=$(iw dev "$wifi_iface" station dump | awk "$awk_cmd") + + if [[ -z "$client_list" ]]; then + echo "No clients connected" + return + fi + + printf "%-20s %-18s %s\n" "MAC" "IP" "Hostname" + + local mac + for mac in $client_list; do + print_client $mac + done + else + die "This option is not supported for the current driver." + fi +} + +has_running_instance() { + local PID x + + mutex_lock + for x in /tmp/create_ap.*; do + if [[ -f $x/pid ]]; then + PID=$(cat $x/pid) + if [[ -d /proc/$PID ]]; then + mutex_unlock + return 0 + fi + fi + done + mutex_lock + + return 1 +} + +is_running_pid() { + list_running | grep -E "^${1} " > /dev/null 2>&1 +} + +send_stop() { + local x + + mutex_lock + # send stop signal to specific pid + if is_running_pid $1; then + kill -USR1 $1 + mutex_unlock + return + fi + + # send stop signal to specific interface + for x in $(list_running | grep -E " \(?${1}( |\)?\$)" | cut -f1 -d' '); do + kill -USR1 $x + done + mutex_unlock +} + +# Storing configs +write_config() { + local i=1 + + # If using pkexec, evaluate permissions before writing. + # However, the /etc/create_ap.conf + # location is excepted. + if [[ "$STORE_CONFIG" != "/etc/create_ap.conf" && $PKEXEC_UID ]]; then + if [ -e "$STORE_CONFIG" ]; then + if ! pkexec --user "$(id -nu $PKEXEC_UID)" test -w "$STORE_CONFIG"; then + echo "ERROR: 1 $(id -nu $PKEXEC_UID) has insufficient permissions to write to config file $STORE_CONFIG" + exit 1 + fi + elif ! pkexec --user "$(id -nu $PKEXEC_UID)" test -w "$(dirname "$STORE_CONFIG")"; then + echo "ERROR: 2 $(id -nu $PKEXEC_UID) has insufficient permissions to write to config file $STORE_CONFIG" + exit 1 + fi + # Assume that the user is making a conf file in a directory they normally + # have control over, and keep permissions strictly private. (i.e. they will + # need to run create_ap directly with sudo in order to write to, say, /etc/create_ap2.conf) + touch "$STORE_CONFIG" + chown "$(id -nu $PKEXEC_UID):$(id -ng $PKEXEC_GID)" "$STORE_CONFIG" + chmod 600 "$STORE_CONFIG" + elif ! eval 'echo -n > "$STORE_CONFIG"' > /dev/null 2>&1; then + echo "ERROR: Unable to create config file $STORE_CONFIG" >&2 + exit 1 + fi + + WIFI_IFACE=$1 + if [[ "$SHARE_METHOD" == "none" ]]; then + SSID="$2" + PASSPHRASE="$3" + else + INTERNET_IFACE="$2" + SSID="$3" + PASSPHRASE="$4" + fi + + if [[ $FREQ_BAND_SET -eq 0 ]]; then + FREQ_BAND="default" + fi + + for config_opt in "${CONFIG_OPTS[@]}"; do + eval echo $config_opt=\$$config_opt + done >> "$STORE_CONFIG" + + echo -e "Config options written to '$STORE_CONFIG'" + exit 0 +} + +is_config_opt() { + local elem opt="$1" + + for elem in "${CONFIG_OPTS[@]}"; do + if [[ "$elem" == "$opt" ]]; then + return 0 + fi + done + return 1 +} + +# Load options from config file +read_config() { + local opt_name opt_val line + + while read line; do + # Read switches and their values + opt_name="${line%%=*}" + opt_val="${line#*=}" + + if [[ $opt_name == "FREQ_BAND" && $opt_val != "default" ]] ; then + FREQ_BAND_SET=1 + fi + + if is_config_opt "$opt_name" ; then + eval $opt_name="\$opt_val" + else + echo "WARN: Unrecognized configuration entry. Please check your config file." >&2 + fi + done < "$LOAD_CONFIG" +} + + +ARGS=( "$@" ) + +FREQ_BAND_SET=0 + +# Preprocessing for --config before option-parsing starts +for ((i=0; i<$#; i++)); do + if [[ "${ARGS[i]}" = "--config" ]]; then + if [[ -f "${ARGS[i+1]}" ]]; then + LOAD_CONFIG="${ARGS[i+1]}" + read_config + else + echo "ERROR: No config file found at given location" >&2 + exit 1 + fi + break + fi +done + +GETOPT_ARGS=$(getopt -o hc:w:g:de:nm: -l "help","hidden","hostapd-debug:","hostapd-timestamps","redirect-to-localhost","mac-filter","mac-filter-accept:","isolate-clients","ieee80211n","ieee80211ac","ieee80211ax","ht_capab:","vht_capab:","driver:","no-virt","fix-unmanaged","country:","freq-band:","mac:","dhcp-dns:","daemon","pidfile:","logfile:","dns-logfile:","stop:","list","list-running","list-clients:","version","psk","no-haveged","no-dns","no-dnsmasq","mkconfig:","config:","dhcp-hosts:" -n "$PROGNAME" -- "$@") +[[ $? -ne 0 ]] && exit 1 +eval set -- "$GETOPT_ARGS" + +while :; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --version) + echo $VERSION + exit 0 + ;; + --hidden) + shift + HIDDEN=1 + ;; + --mac-filter) + shift + MAC_FILTER=1 + ;; + --mac-filter-accept) + shift + MAC_FILTER_ACCEPT="$1" + shift + ;; + --isolate-clients) + shift + ISOLATE_CLIENTS=1 + ;; + -c) + shift + CHANNEL="$1" + shift + ;; + -w) + shift + WPA_VERSION="$1" + [[ "$WPA_VERSION" == "2+1" ]] && WPA_VERSION=1+2 + shift + ;; + -g) + shift + GATEWAY="$1" + shift + ;; + -d) + shift + ETC_HOSTS=1 + ;; + -e) + shift + ADDN_HOSTS="$1" + shift + ;; + --dhcp-hosts) + shift + DHCP_HOSTS="$1" + shift + ;; + -n) + shift + SHARE_METHOD=none + ;; + -m) + shift + SHARE_METHOD="$1" + shift + ;; + --ieee80211n) + shift + IEEE80211N=1 + ;; + --ieee80211ac) + shift + IEEE80211AC=1 + ;; + --ieee80211ax) + shift + IEEE80211AX=1 + ;; + --ht_capab) + shift + HT_CAPAB="$1" + shift + ;; + --vht_capab) + shift + VHT_CAPAB="$1" + shift + ;; + --driver) + shift + DRIVER="$1" + shift + ;; + --no-virt) + shift + NO_VIRT=1 + ;; + --fix-unmanaged) + shift + FIX_UNMANAGED=1 + ;; + --country) + shift + COUNTRY="$1" + shift + ;; + --freq-band) + shift + FREQ_BAND="$1" + FREQ_BAND_SET=1 + shift + ;; + --mac) + shift + NEW_MACADDR="$1" + shift + ;; + --dhcp-dns) + shift + DHCP_DNS="$1" + shift + ;; + --daemon) + shift + DAEMONIZE=1 + ;; + --pidfile) + shift + DAEMON_PIDFILE="$1" + shift + ;; + --logfile) + shift + DAEMON_LOGFILE="$1" + shift + ;; + --dns-logfile) + shift + DNS_LOGFILE="$1" + shift + ;; + --stop) + shift + STOP_ID="$1" + shift + ;; + --list) + shift + LIST_RUNNING=1 + echo -e "WARN: --list is deprecated, use --list-running instead.\n" >&2 + ;; + --list-running) + shift + LIST_RUNNING=1 + ;; + --list-clients) + shift + LIST_CLIENTS_ID="$1" + shift + ;; + --no-haveged) + shift + NO_HAVEGED=1 + ;; + --psk) + shift + USE_PSK=1 + ;; + --no-dns) + shift + NO_DNS=1 + ;; + --no-dnsmasq) + shift + NO_DNSMASQ=1 + ;; + --redirect-to-localhost) + shift + REDIRECT_TO_LOCALHOST=1 + ;; + --hostapd-debug) + shift + if [ "x$1" = "x1" ]; then + HOSTAPD_DEBUG_ARGS+="-d " + elif [ "x$1" = "x2" ]; then + HOSTAPD_DEBUG_ARGS+="-dd " + else + printf "Error: argument for --hostapd-debug expected 1 or 2, got %s\n" "$1" + exit 1 + fi + shift + ;; + --hostapd-timestamps) + shift + HOSTAPD_DEBUG_ARGS+="-t " + ;; + --mkconfig) + shift + STORE_CONFIG="$1" + shift + ;; + --config) + shift + shift + ;; + --) + shift + break + ;; + esac +done + +# Load positional args from config file, if needed +if [[ -n "$LOAD_CONFIG" && $# -eq 0 ]]; then + i=0 + # set arguments in order + for x in WIFI_IFACE INTERNET_IFACE SSID PASSPHRASE; do + if eval "[[ -n \"\$${x}\" ]]"; then + eval "set -- \"\${@:1:$i}\" \"\$${x}\"" + ((i++)) + fi + # we unset the variable to avoid any problems later + eval "unset $x" + done +fi + +# Check if required number of positional args are present +if [[ $# -lt 1 && $FIX_UNMANAGED -eq 0 && -z "$STOP_ID" && + $LIST_RUNNING -eq 0 && -z "$LIST_CLIENTS_ID" ]]; then + usage >&2 + exit 1 +fi + +# Set NO_DNS, if dnsmasq is disabled +if [[ $NO_DNSMASQ -eq 1 ]]; then + NO_DNS=1 +fi + +trap "cleanup_lock" EXIT + +if [[ $(id -u) -ne 0 ]]; then + echo "create_ap must be run as root." >&2 + exit 1 +fi + +if ! init_lock; then + echo "ERROR: Failed to initialize lock" >&2 + exit 1 +fi + +# if the user press ctrl+c or we get USR1 signal +# then run clean_exit() +trap "clean_exit" SIGINT SIGUSR1 +# if we get USR2 signal then run die(). +trap "die" SIGUSR2 + +[[ -n "$STORE_CONFIG" ]] && write_config "$@" + +if [[ $LIST_RUNNING -eq 1 ]]; then + #echo -e "List of running $PROGNAME instances:\n" + list_running + exit 0 +fi + +if [[ -n "$LIST_CLIENTS_ID" ]]; then + list_clients "$LIST_CLIENTS_ID" + exit 0 +fi + +if [[ -n "$STOP_ID" ]]; then + echo "Trying to kill $PROGNAME instance associated with $STOP_ID..." + send_stop "$STOP_ID" + exit 0 +fi + +if [[ $FIX_UNMANAGED -eq 1 ]]; then + echo "Trying to fix unmanaged status in NetworkManager..." + networkmanager_fix_unmanaged + exit 0 +fi + +if [[ $DAEMONIZE -eq 1 && $RUNNING_AS_DAEMON -eq 0 ]]; then + # Assume we're running underneath a service manager if PIDFILE is set + # and don't clobber it's output with a useless message + if [ -z "$DAEMON_PIDFILE" ]; then + echo "Running as Daemon..." + fi + # run a detached create_ap + RUNNING_AS_DAEMON=1 setsid "$0" "${ARGS[@]}" >>$DAEMON_LOGFILE 2>&1 & + exit 0 +elif [[ $RUNNING_AS_DAEMON -eq 1 && -n "$DAEMON_PIDFILE" ]]; then + echo $$ >$DAEMON_PIDFILE +fi + +if [[ $FREQ_BAND_SET != 0 ]]; then + if [[ $FREQ_BAND != 2.4 && $FREQ_BAND != 5 ]]; then + echo "ERROR: Invalid frequency band" >&2 + exit 1 + fi +fi + +if [[ $CHANNEL == default ]]; then + USING_DEFAULT_CHANNEL=1 + if [[ $FREQ_BAND == 2.4 ]]; then + CHANNEL=1 + else + CHANNEL=36 + fi +else + USING_DEFAULT_CHANNEL=0 +fi + + +if [[ $FREQ_BAND != 5 && $CHANNEL -gt 14 ]]; then + echo "Channel number is greater than 14, assuming 5GHz frequency band" + FREQ_BAND=5 +fi + +WIFI_IFACE=$1 + +if ! is_wifi_interface ${WIFI_IFACE}; then + echo "ERROR: '${WIFI_IFACE}' is not a WiFi interface" >&2 + exit 1 +fi + +if ! can_be_ap ${WIFI_IFACE}; then + echo "ERROR: Your adapter does not support AP (master) mode" >&2 + exit 1 +fi + +if ! can_be_sta_and_ap ${WIFI_IFACE}; then + if is_wifi_connected ${WIFI_IFACE}; then + echo "ERROR: Your adapter can not be a station (i.e. be connected) and an AP at the same time" >&2 + exit 1 + elif [[ $NO_VIRT -eq 0 ]]; then + echo "WARN: Your adapter does not fully support AP virtual interface, enabling --no-virt" >&2 + NO_VIRT=1 + fi +fi + +HOSTAPD=$(which hostapd) + +if [[ ! -x "$HOSTAPD" ]]; then + echo "ERROR: hostapd not found." >&2 + exit 1 +fi + +if [[ $(get_adapter_kernel_module ${WIFI_IFACE}) =~ ^(8192[cd][ue]|8723a[sue])$ ]]; then + if ! strings "$HOSTAPD" | grep -m1 rtl871xdrv > /dev/null 2>&1; then + echo "ERROR: You need to patch your hostapd with rtl871xdrv patches." >&2 + exit 1 + fi + + if [[ $DRIVER != "rtl871xdrv" ]]; then + echo "WARN: Your adapter needs rtl871xdrv, enabling --driver=rtl871xdrv" >&2 + DRIVER=rtl871xdrv + fi +fi + +if [[ "$SHARE_METHOD" != "nat" && "$SHARE_METHOD" != "bridge" && "$SHARE_METHOD" != "none" ]]; then + echo "ERROR: Wrong Internet sharing method" >&2 + echo + usage >&2 + exit 1 +fi + +if [[ -n "$NEW_MACADDR" ]]; then + if ! is_macaddr "$NEW_MACADDR"; then + echo "ERROR: '${NEW_MACADDR}' is not a valid MAC address" >&2 + exit 1 + fi + + if ! is_unicast_macaddr "$NEW_MACADDR"; then + echo "ERROR: The first byte of MAC address (${NEW_MACADDR}) must be even" >&2 + exit 1 + fi + + if [[ $(get_all_macaddrs | grep -c ${NEW_MACADDR}) -ne 0 ]]; then + echo "WARN: MAC address '${NEW_MACADDR}' already exists. Because of this, you may encounter some problems" >&2 + fi +fi + +if [[ "$SHARE_METHOD" != "none" ]]; then + MIN_REQUIRED_ARGS=2 +else + MIN_REQUIRED_ARGS=1 +fi + +if [[ $# -gt $MIN_REQUIRED_ARGS ]]; then + if [[ "$SHARE_METHOD" != "none" ]]; then + if [[ $# -ne 3 && $# -ne 4 ]]; then + usage >&2 + exit 1 + fi + INTERNET_IFACE="$2" + SSID="$3" + PASSPHRASE="$4" + else + if [[ $# -ne 2 && $# -ne 3 ]]; then + usage >&2 + exit 1 + fi + SSID="$2" + PASSPHRASE="$3" + fi +else + if [[ "$SHARE_METHOD" != "none" ]]; then + if [[ $# -ne 2 ]]; then + usage >&2 + exit 1 + fi + INTERNET_IFACE="$2" + fi + if tty -s; then + while :; do + read -p "SSID: " SSID + if [[ ${#SSID} -lt 1 || ${#SSID} -gt 32 ]]; then + echo "ERROR: Invalid SSID length ${#SSID} (expected 1..32)" >&2 + continue + fi + break + done + while :; do + if [[ $USE_PSK -eq 0 ]]; then + read -p "Passphrase: " -s PASSPHRASE + echo + if [[ ${#PASSPHRASE} -gt 0 && ${#PASSPHRASE} -lt 8 ]] || [[ ${#PASSPHRASE} -gt 63 ]]; then + echo "ERROR: Invalid passphrase length ${#PASSPHRASE} (expected 8..63)" >&2 + continue + fi + read -p "Retype passphrase: " -s PASSPHRASE2 + echo + if [[ "$PASSPHRASE" != "$PASSPHRASE2" ]]; then + echo "Passphrases do not match." + else + break + fi + else + read -p "PSK: " PASSPHRASE + echo + if [[ ${#PASSPHRASE} -gt 0 && ${#PASSPHRASE} -ne 64 ]]; then + echo "ERROR: Invalid pre-shared-key length ${#PASSPHRASE} (expected 64)" >&2 + continue + fi + fi + done + else + read SSID + read PASSPHRASE + fi +fi + +if [[ "$SHARE_METHOD" != "none" ]] && ! is_interface $INTERNET_IFACE; then + echo "ERROR: '${INTERNET_IFACE}' is not an interface" >&2 + exit 1 +fi + +if [[ ${#SSID} -lt 1 || ${#SSID} -gt 32 ]]; then + echo "ERROR: Invalid SSID length ${#SSID} (expected 1..32)" >&2 + exit 1 +fi + +if [[ $USE_PSK -eq 0 ]]; then + if [[ ${#PASSPHRASE} -gt 0 && ${#PASSPHRASE} -lt 8 ]] || [[ ${#PASSPHRASE} -gt 63 ]]; then + echo "ERROR: Invalid passphrase length ${#PASSPHRASE} (expected 8..63)" >&2 + exit 1 + fi +elif [[ ${#PASSPHRASE} -gt 0 && ${#PASSPHRASE} -ne 64 ]]; then + echo "ERROR: Invalid pre-shared-key length ${#PASSPHRASE} (expected 64)" >&2 + exit 1 +fi + +if [[ $(get_adapter_kernel_module ${WIFI_IFACE}) =~ ^rtl[0-9].*$ ]]; then + if [[ -n "$PASSPHRASE" ]]; then + echo "WARN: Realtek drivers usually have problems with WPA1, enabling -w 2" >&2 + WPA_VERSION=2 + fi + echo "WARN: If AP doesn't work, please read: howto/realtek.md" >&2 +fi + +if [[ $NO_VIRT -eq 1 && "$WIFI_IFACE" == "$INTERNET_IFACE" ]]; then + echo -n "ERROR: You can not share your connection from the same" >&2 + echo " interface if you are using --no-virt option." >&2 + exit 1 +fi + +mutex_lock +trap "cleanup" EXIT +CONFDIR=$(mktemp -d /tmp/create_ap.${WIFI_IFACE}.conf.XXXXXXXX) +echo "Config dir: $CONFDIR" +echo "PID: $$" +echo $$ > $CONFDIR/pid + +# to make --list-running work from any user, we must give read +# permissions to $CONFDIR and $CONFDIR/pid +chmod 755 $CONFDIR +chmod 444 $CONFDIR/pid + +COMMON_CONFDIR=/tmp/create_ap.common.conf +mkdir -p $COMMON_CONFDIR + +if [[ "$SHARE_METHOD" == "nat" ]]; then + echo $INTERNET_IFACE > $CONFDIR/nat_internet_iface + cp_n /proc/sys/net/ipv4/conf/$INTERNET_IFACE/forwarding \ + $COMMON_CONFDIR/${INTERNET_IFACE}_forwarding +fi +cp_n /proc/sys/net/ipv4/ip_forward $COMMON_CONFDIR +if [[ -e /proc/sys/net/bridge/bridge-nf-call-iptables ]]; then + cp_n /proc/sys/net/bridge/bridge-nf-call-iptables $COMMON_CONFDIR +fi +mutex_unlock + +if [[ "$SHARE_METHOD" == "bridge" ]]; then + if is_bridge_interface $INTERNET_IFACE; then + BRIDGE_IFACE=$INTERNET_IFACE + else + BRIDGE_IFACE=$(alloc_new_iface br) + fi +fi + +if [[ $USE_IWCONFIG -eq 0 ]]; then + iw dev ${WIFI_IFACE} set power_save off +fi + +if [[ $NO_VIRT -eq 0 ]]; then + VWIFI_IFACE=$(alloc_new_iface ap) + + # in NetworkManager 0.9.9 and above we can set the interface as unmanaged without + # the need of MAC address, so we set it before we create the virtual interface. + if networkmanager_is_running && [[ $NM_OLDER_VERSION -eq 0 ]]; then + echo -n "Network Manager found, set ${VWIFI_IFACE} as unmanaged device... " + networkmanager_add_unmanaged ${VWIFI_IFACE} + # do not call networkmanager_wait_until_unmanaged because interface does not + # exist yet + echo "DONE" + fi + + + if is_wifi_connected ${WIFI_IFACE} && [[ $FREQ_BAND_SET -eq 0 ]]; then + WIFI_IFACE_FREQ=$(iw dev ${WIFI_IFACE} link | grep -i freq | awk '{print $2}') + WIFI_IFACE_CHANNEL=$(ieee80211_frequency_to_channel ${WIFI_IFACE_FREQ}) + echo -n "${WIFI_IFACE} is already associated with channel ${WIFI_IFACE_CHANNEL} (${WIFI_IFACE_FREQ} MHz)" + if is_5ghz_frequency $WIFI_IFACE_FREQ; then + FREQ_BAND=5 + else + FREQ_BAND=2.4 + fi + if [[ $WIFI_IFACE_CHANNEL -ne $CHANNEL ]]; then + if ( get_adapter_info ${IFACE} | grep "#channels <= 2" -q ) + then + echo -e "\nmultiple channels supported" + else + echo -e "\nmultiple channels not supported", + echo -e "\nfallback to channel ${WIFI_IFACE_CHANNEL}" + CHANNEL=$WIFI_IFACE_CHANNEL + fi + else + echo "channel------------------ ${CHANNEL}" + fi + elif is_wifi_connected ${WIFI_IFACE} && [[ $FREQ_BAND_SET -eq 1 ]]; then + echo "Custom frequency band set with ${FREQ_BAND}Ghz with channel ${CHANNEL}" + fi + + + VIRTDIEMSG="Maybe your WiFi adapter does not fully support virtual interfaces. + Try again with --no-virt." + echo -n "Creating a virtual WiFi interface... " + + if iw dev ${WIFI_IFACE} interface add ${VWIFI_IFACE} type __ap; then + # now we can call networkmanager_wait_until_unmanaged + networkmanager_is_running && [[ $NM_OLDER_VERSION -eq 0 ]] && networkmanager_wait_until_unmanaged ${VWIFI_IFACE} + echo "${VWIFI_IFACE} created." + else + VWIFI_IFACE= + die "$VIRTDIEMSG" + fi + OLD_MACADDR=$(get_macaddr ${VWIFI_IFACE}) + if [[ -z "$NEW_MACADDR" && $(get_all_macaddrs | grep -c ${OLD_MACADDR}) -ne 1 ]]; then + NEW_MACADDR=$(get_new_macaddr ${VWIFI_IFACE}) + fi + WIFI_IFACE=${VWIFI_IFACE} +else + OLD_MACADDR=$(get_macaddr ${WIFI_IFACE}) +fi + +mutex_lock +echo $WIFI_IFACE > $CONFDIR/wifi_iface +chmod 444 $CONFDIR/wifi_iface +mutex_unlock + +if [[ -n "$COUNTRY" && $USE_IWCONFIG -eq 0 ]]; then + iw reg set "$COUNTRY" +fi + +# Fallback to currently connected channel if the adapter can not transmit to the default channel (1) +if can_transmit_to_channel "${WIFI_IFACE}" "${CHANNEL}"; then + echo "Transmitting to channel ${CHANNEL}..." +else + if [[ $USING_DEFAULT_CHANNEL -eq 1 && $WIFI_IFACE_CHANNEL -ne $CHANNEL ]]; then + echo -e "Your adapter can not transmit to channel ${CHANNEL}" >&2 + CHANNEL=$WIFI_IFACE_CHANNEL + echo -e "Falling back to channel ${CHANNEL}" + can_transmit_to_channel "${WIFI_IFACE}" "${CHANNEL}" || die "Your adapter can not transmit to channel ${CHANNEL}, frequency band ${FREQ_BAND}GHz." + else + die "Your adapter can not transmit to channel ${CHANNEL}, frequency band ${FREQ_BAND}GHz." + fi +fi + +if networkmanager_exists && ! networkmanager_iface_is_unmanaged ${WIFI_IFACE}; then + echo -n "Network Manager found, set ${WIFI_IFACE} as unmanaged device... " + networkmanager_add_unmanaged ${WIFI_IFACE} + + if networkmanager_is_running; then + networkmanager_wait_until_unmanaged ${WIFI_IFACE} + fi + + echo "DONE" +fi + +[[ $HIDDEN -eq 1 ]] && echo "Access Point's SSID is hidden!" + +[[ $MAC_FILTER -eq 1 ]] && echo "MAC address filtering is enabled!" + +[[ $ISOLATE_CLIENTS -eq 1 ]] && echo "Access Point's clients will be isolated!" + +# hostapd config +cat << EOF > $CONFDIR/hostapd.conf +beacon_int=100 +ssid=${SSID} +interface=${WIFI_IFACE} +driver=${DRIVER} +channel=${CHANNEL} +ctrl_interface=$CONFDIR/hostapd_ctrl +ctrl_interface_group=0 +ignore_broadcast_ssid=$HIDDEN +ap_isolate=$ISOLATE_CLIENTS +EOF + +if [[ -n "$COUNTRY" ]]; then + cat << EOF >> $CONFDIR/hostapd.conf +country_code=${COUNTRY} +ieee80211d=1 +EOF +fi + +if [[ $FREQ_BAND == 2.4 ]]; then + echo "hw_mode=g" >> $CONFDIR/hostapd.conf +else + echo "hw_mode=a" >> $CONFDIR/hostapd.conf +fi + +if [[ $MAC_FILTER -eq 1 ]]; then + cat << EOF >> $CONFDIR/hostapd.conf +macaddr_acl=${MAC_FILTER} +accept_mac_file=${MAC_FILTER_ACCEPT} +EOF +fi + +if [[ $IEEE80211N -eq 1 ]]; then + cat << EOF >> $CONFDIR/hostapd.conf +ieee80211n=1 +ht_capab=${HT_CAPAB} +EOF +fi + +if [[ $IEEE80211AC -eq 1 ]]; then + echo "ieee80211ac=1" >> $CONFDIR/hostapd.conf +fi + +if [[ $IEEE80211AX -eq 1 ]]; then + echo "ieee80211ax=1" >> $CONFDIR/hostapd.conf +fi + +if [[ -n "$VHT_CAPAB" ]]; then + echo "vht_capab=${VHT_CAPAB}" >> $CONFDIR/hostapd.conf +fi + +if [[ $IEEE80211N -eq 1 ]] || [[ $IEEE80211AC -eq 1 ]]; then + echo "wmm_enabled=1" >> $CONFDIR/hostapd.conf +fi + +if [[ -n "$PASSPHRASE" ]]; then + if [[ "$WPA_VERSION" == "1+2" ]]; then + WPA_VERSION=2 # Assuming you want to default to WPA2 for the "1+2" setting + fi + if [[ $USE_PSK -eq 0 ]]; then + WPA_KEY_TYPE=passphrase + else + WPA_KEY_TYPE=psk + fi + + if [[ "$WPA_VERSION" == "3" ]]; then + # Configuring for WPA3 Transition Mode + # 80211w must be 1 or Apple Devices will not connect. + # 1 is the only valid value for WPA3 Transition Mode + cat << EOF >> $CONFDIR/hostapd.conf +wpa=2 +wpa_${WPA_KEY_TYPE}=${PASSPHRASE} +wpa_key_mgmt=WPA-PSK SAE +wpa_pairwise=CCMP +rsn_pairwise=CCMP +ieee80211w=1 +EOF + else + # Original configuration for WPA_VERSION other than 3 + cat << EOF >> $CONFDIR/hostapd.conf +wpa=${WPA_VERSION} +wpa_${WPA_KEY_TYPE}=${PASSPHRASE} +wpa_key_mgmt=WPA-PSK +wpa_pairwise=CCMP +rsn_pairwise=CCMP +EOF + fi +fi + + +if [[ "$SHARE_METHOD" == "bridge" ]]; then + echo "bridge=${BRIDGE_IFACE}" >> $CONFDIR/hostapd.conf +elif [[ $NO_DNSMASQ -eq 0 ]]; then + # dnsmasq config (dhcp + dns) + DNSMASQ_VER=$(dnsmasq -v | grep -m1 -oE '[0-9]+(\.[0-9]+)*\.[0-9]+') + version_cmp $DNSMASQ_VER 2.63 + if [[ $? -eq 1 ]]; then + DNSMASQ_BIND=bind-interfaces + else + DNSMASQ_BIND=bind-dynamic + fi + if [[ "$DHCP_DNS" == "gateway" ]]; then + DHCP_DNS="$GATEWAY" + fi + cat << EOF > $CONFDIR/dnsmasq.conf +listen-address=${GATEWAY} +${DNSMASQ_BIND} +dhcp-range=${GATEWAY%.*}.1,${GATEWAY%.*}.254,255.255.255.0,24h +dhcp-option-force=option:router,${GATEWAY} +dhcp-option-force=option:dns-server,${DHCP_DNS} +EOF + MTU=$(get_mtu $INTERNET_IFACE) + [[ -n "$MTU" ]] && echo "dhcp-option-force=option:mtu,${MTU}" >> $CONFDIR/dnsmasq.conf + [[ $ETC_HOSTS -eq 0 ]] && echo no-hosts >> $CONFDIR/dnsmasq.conf + [[ -n "$ADDN_HOSTS" ]] && echo "addn-hosts=${ADDN_HOSTS}" >> $CONFDIR/dnsmasq.conf + if [[ -n "$DHCP_HOSTS" ]]; then + for HOST in $DHCP_HOSTS; do + echo "dhcp-host=${HOST}" >> $CONFDIR/dnsmasq.conf + done + fi + + + if [[ -n "$DNS_LOGFILE" ]]; then + cat << EOF >> $CONFDIR/dnsmasq.conf +log-queries +log-facility=${DNS_LOGFILE} +EOF + fi + if [[ "$SHARE_METHOD" == "none" && "$REDIRECT_TO_LOCALHOST" == "1" ]]; then + cat << EOF >> $CONFDIR/dnsmasq.conf +address=/#/$GATEWAY +EOF + fi +fi + +# initialize WiFi interface +if [[ $NO_VIRT -eq 0 && -n "$NEW_MACADDR" ]]; then + ip link set dev ${WIFI_IFACE} address ${NEW_MACADDR} || die "$VIRTDIEMSG" +fi + +ip link set down dev ${WIFI_IFACE} || die "$VIRTDIEMSG" +ip addr flush ${WIFI_IFACE} || die "$VIRTDIEMSG" + +if [[ $NO_VIRT -eq 1 && -n "$NEW_MACADDR" ]]; then + ip link set dev ${WIFI_IFACE} address ${NEW_MACADDR} || die +fi + +if [[ "$SHARE_METHOD" != "bridge" ]]; then + ip link set up dev ${WIFI_IFACE} || die "$VIRTDIEMSG" + ip addr add ${GATEWAY}/24 broadcast ${GATEWAY%.*}.255 dev ${WIFI_IFACE} || die "$VIRTDIEMSG" +fi + +# enable Internet sharing +if [[ "$SHARE_METHOD" != "none" ]]; then + echo "Sharing Internet using method: $SHARE_METHOD" + if [[ "$SHARE_METHOD" == "nat" ]]; then + iptables -w -t nat -I POSTROUTING -s ${GATEWAY%.*}.0/24 ! -o ${WIFI_IFACE} -j MASQUERADE || die + iptables -w -I FORWARD -i ${WIFI_IFACE} -s ${GATEWAY%.*}.0/24 -j ACCEPT || die + iptables -w -I FORWARD -i ${INTERNET_IFACE} -d ${GATEWAY%.*}.0/24 -j ACCEPT || die + echo 1 > /proc/sys/net/ipv4/conf/$INTERNET_IFACE/forwarding || die + echo 1 > /proc/sys/net/ipv4/ip_forward || die + # to enable clients to establish PPTP connections we must + # load nf_nat_pptp module + modprobe nf_nat_pptp > /dev/null 2>&1 + elif [[ "$SHARE_METHOD" == "bridge" ]]; then + # disable iptables rules for bridged interfaces + if [[ -e /proc/sys/net/bridge/bridge-nf-call-iptables ]]; then + echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables + fi + + # to initialize the bridge interface correctly we need to do the following: + # + # 1) save the IPs and route table of INTERNET_IFACE + # 2) if NetworkManager is running set INTERNET_IFACE as unmanaged + # 3) create BRIDGE_IFACE and attach INTERNET_IFACE to it + # 4) set the previously saved IPs and route table to BRIDGE_IFACE + # + # we need the above because BRIDGE_IFACE is the master interface from now on + # and it must know where is connected, otherwise connection is lost. + if ! is_bridge_interface $INTERNET_IFACE; then + echo -n "Create a bridge interface... " + OLD_IFS="$IFS" + IFS=$'\n' + + IP_ADDRS=( $(ip addr show $INTERNET_IFACE | grep -A 1 -E 'inet[[:blank:]]' | paste - -) ) + ROUTE_ADDRS=( $(ip route show dev $INTERNET_IFACE) ) + + IFS="$OLD_IFS" + + if networkmanager_is_running; then + networkmanager_add_unmanaged $INTERNET_IFACE + networkmanager_wait_until_unmanaged $INTERNET_IFACE + fi + + # create bridge interface + ip link add name $BRIDGE_IFACE type bridge || die + ip link set dev $BRIDGE_IFACE up || die + # set 0ms forward delay + echo -n 0 > /sys/class/net/$BRIDGE_IFACE/bridge/forward_delay + + # attach internet interface to bridge interface + ip link set dev $INTERNET_IFACE promisc on || die + ip link set dev $INTERNET_IFACE up || die + ip link set dev $INTERNET_IFACE master $BRIDGE_IFACE || die + + ip addr flush $INTERNET_IFACE + for x in "${IP_ADDRS[@]}"; do + x="${x/inet/}" + x="${x/secondary/}" + x="${x/dynamic/}" + x=$(echo $x | sed 's/\([0-9]\)sec/\1/g') + x="${x/${INTERNET_IFACE}/}" + ip addr add $x dev $BRIDGE_IFACE || die + done + + # remove any existing entries that were added from 'ip addr add' + ip route flush dev $INTERNET_IFACE + ip route flush dev $BRIDGE_IFACE + + # we must first add the entries that specify the subnets and then the + # gateway entry, otherwise 'ip addr add' will return an error + for x in "${ROUTE_ADDRS[@]}"; do + [[ "$x" == default* ]] && continue + ip route add $x dev $BRIDGE_IFACE || die + done + + for x in "${ROUTE_ADDRS[@]}"; do + [[ "$x" != default* ]] && continue + ip route add $x dev $BRIDGE_IFACE || die + done + + echo "$BRIDGE_IFACE created." + fi + fi +else + echo "No Internet sharing" +fi + +# start dhcp + dns (optional) +if [[ "$SHARE_METHOD" != "bridge" ]]; then + if [[ $NO_DNS -eq 0 ]]; then + DNS_PORT=5353 + iptables -w -I INPUT -p tcp -m tcp --dport $DNS_PORT -j ACCEPT || die + iptables -w -I INPUT -p udp -m udp --dport $DNS_PORT -j ACCEPT || die + iptables -w -t nat -I PREROUTING -s ${GATEWAY%.*}.0/24 -d ${GATEWAY} \ + -p tcp -m tcp --dport 53 -j REDIRECT --to-ports $DNS_PORT || die + iptables -w -t nat -I PREROUTING -s ${GATEWAY%.*}.0/24 -d ${GATEWAY} \ + -p udp -m udp --dport 53 -j REDIRECT --to-ports $DNS_PORT || die + else + DNS_PORT=0 + fi + + if [[ $NO_DNSMASQ -eq 0 ]]; then + iptables -w -I INPUT -p udp -m udp --dport 67 -j ACCEPT || die + + + # apparmor does not allow dnsmasq to read files. + # remove restriction. + + if COMPLAIN_CMD=$(command -v complain || command -v aa-complain); then + $COMPLAIN_CMD dnsmasq + fi + + umask 0033 + dnsmasq -C $CONFDIR/dnsmasq.conf -x $CONFDIR/dnsmasq.pid -l $CONFDIR/dnsmasq.leases -p $DNS_PORT || die + umask $SCRIPT_UMASK + fi +fi + +# start access point +echo "hostapd command-line interface: hostapd_cli -p $CONFDIR/hostapd_ctrl" + +if [[ $NO_HAVEGED -eq 0 ]]; then + haveged_watchdog & + HAVEGED_WATCHDOG_PID=$! +fi + +# start hostapd (use stdbuf when available for no delayed output in programs that redirect stdout) +STDBUF_PATH=`which stdbuf` +if [ $? -eq 0 ]; then + STDBUF_PATH=$STDBUF_PATH" -oL" +fi +$STDBUF_PATH $HOSTAPD $HOSTAPD_DEBUG_ARGS $CONFDIR/hostapd.conf & +HOSTAPD_PID=$! +echo $HOSTAPD_PID > $CONFDIR/hostapd.pid + +if ! wait $HOSTAPD_PID; then + echo -e "\nError: Failed to run hostapd, maybe a program is interfering." >&2 + if networkmanager_is_running; then + echo "If an error like 'n80211: Could not configure driver mode' was thrown" >&2 + echo "try running the following before starting create_ap:" >&2 + if [[ $NM_OLDER_VERSION -eq 1 ]]; then + echo " nmcli nm wifi off" >&2 + else + echo " nmcli r wifi off" >&2 + fi + echo " rfkill unblock wlan" >&2 + fi + die +fi + +clean_exit + +# Local Variables: +# tab-width: 4 +# indent-tabs-mode: nil +# End: + +# vim: et sts=4 sw=4 diff --git a/x64_bin/wihotspot b/x64_bin/wihotspot new file mode 100755 index 0000000..689b2ea --- /dev/null +++ b/x64_bin/wihotspot @@ -0,0 +1,4 @@ +#!/bin/sh + +# Start wihotspot-gui +/usr/bin/wihotspot-gui diff --git a/x64_bin/wihotspot-gui b/x64_bin/wihotspot-gui new file mode 100755 index 0000000..5d2e527 Binary files /dev/null and b/x64_bin/wihotspot-gui differ