Compare commits
17 Commits
GUI
..
entertaiment
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e1c06a63e | |||
| 5244c21ba3 | |||
| e62626b55c | |||
| 494eae2bdf | |||
| 6d3482939e | |||
| fe751bbc0c | |||
| d591e71363 | |||
| 93f3044c2a | |||
| 49fb248454 | |||
| 0c9d09c331 | |||
| 070524bafb | |||
| e7c8c176ed | |||
| db7bf4de86 | |||
| 60718b360e | |||
| 54592a5839 | |||
| 2a2b4d81d1 | |||
| cf7ba2d58a |
@@ -19,3 +19,5 @@ Thumbs.db
|
|||||||
# Docker Compose — user's active stacks and secrets
|
# Docker Compose — user's active stacks and secrets
|
||||||
compose/custom/
|
compose/custom/
|
||||||
compose/config.env
|
compose/config.env
|
||||||
|
|
||||||
|
session
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
echo "[+] Linux_post_install — Web GUI"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# ── Check python3 ──────────────────────────────────────────────
|
|
||||||
if ! command -v python3 &>/dev/null; then
|
|
||||||
echo "ERROR: python3 not found — install it first: sudo apt install python3 python3-pip"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Cache sudo credentials ─────────────────────────────────────
|
|
||||||
echo "[!] Root access is required for some operations (install, firewall, etc.)"
|
|
||||||
echo " Your credentials will be cached for the session."
|
|
||||||
sudo -v
|
|
||||||
# Keep sudo alive in background
|
|
||||||
while true; do sudo -n true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
|
|
||||||
|
|
||||||
# ── Install Python deps ────────────────────────────────────────
|
|
||||||
echo "[+] Installing Python dependencies..."
|
|
||||||
python3 -m pip install --quiet -r gui/requirements.txt 2>/dev/null || {
|
|
||||||
python3 -m pip install --user --quiet -r gui/requirements.txt
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Launch ─────────────────────────────────────────────────────
|
|
||||||
PORT="${PORT:-8080}"
|
|
||||||
HOST="${HOST:-0.0.0.0}"
|
|
||||||
|
|
||||||
echo "[+] Starting web GUI at http://$HOST:$PORT"
|
|
||||||
echo " Press Ctrl+C to stop."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
python3 -m uvicorn gui.main:app --host "$HOST" --port "$PORT" --reload
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Linux_post_install — Agent Instructions
|
||||||
|
|
||||||
|
Personal bootstrap & homelab toolkit for Debian/Ubuntu (Bash). `install.sh` bootstraps a machine; `bin/pos` is the unified CLI.
|
||||||
|
|
||||||
|
## External File Loading
|
||||||
|
|
||||||
|
CRITICAL: real guidance lives in DOC/. When you encounter a reference below, use your Read tool to load it on a need-to-know basis — do NOT preemptively load all of them. Once loaded, treat the content as mandatory instructions.
|
||||||
|
|
||||||
|
- @DOC/AGENT_Context_Project.md — project overview, directory structure, `pos` dispatch table, "How to modify" table. Read FIRST for any non-trivial task.
|
||||||
|
- @DOC/DEV.md — conventions, verification, and the "Adding a new Feature/App/Tool" checklists. Read before creating or changing code/docs.
|
||||||
|
- @DOC/POS.md — `pos` CLI reference (dispatcher + every command). Read when working on `bin/pos*` scripts or their docs.
|
||||||
|
- @DOC/README.md — index of all docs. Read to find the right doc.
|
||||||
|
|
||||||
|
## Quick facts
|
||||||
|
|
||||||
|
- Each tool is `bin/pos-<category>-<command>`; `bin/pos` dispatches via smart arg matching; bash completion derives from filenames.
|
||||||
|
- Verify edits with `bash -n` on touched scripts and smoke-test dispatch; keep line counts in `DOC/AGENT_Context_Project.md`'s file table in sync.
|
||||||
@@ -29,26 +29,48 @@ Linux_post_install/
|
|||||||
├── postinstall.sh # Phase 3: PATH, bash completion, systemd services
|
├── postinstall.sh # Phase 3: PATH, bash completion, systemd services
|
||||||
│
|
│
|
||||||
├── lib/
|
├── lib/
|
||||||
│ └── common.sh # Shared library (colors, logging, spinner, timer, run)
|
│ ├── common.sh # Shared library (colors, logging, spinner, timer, run)
|
||||||
|
│ └── flags.sh # Feature flag store (flag_set/clear/is_set/value/list/status)
|
||||||
│
|
│
|
||||||
├── bin/ # CLI tools — installed to /usr/local/bin/
|
├── bin/ # CLI tools — installed to /usr/local/bin/
|
||||||
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
|
||||||
│ ├── pos-network-ip # Show interfaces, routes, public IP
|
<!-- GEN:START tree -->
|
||||||
│ ├── pos-network-checkport # TCP port checker
|
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||||
│ ├── pos-network-scan # Parallel ping sweep of CIDR subnet
|
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
|
||||||
│ ├── pos-docker-ps # Enhanced docker ps (health, IPs, ports, uptime)
|
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
|
||||||
│ ├── pos-docker-health # Quick one-glance health dashboard
|
│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
|
||||||
│ ├── pos-docker-compose # Docker Compose service manager (largest script, 317 lines)
|
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
||||||
│ ├── pos-media-mp3 # Audio downloader (yt-dlp → MP3)
|
│ ├── pos-media-mp4 # Download video as MP4 (interactive format select)
|
||||||
│ ├── pos-media-mp4 # Video downloader (yt-dlp → MP4, interactive format select)
|
│ ├── pos-network-checkport # Check TCP port connectivity
|
||||||
│ ├── pos-system-firewall # Interactive UFW manager (menu-driven, 284 lines)
|
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||||
│ ├── pos-ssh-load-keys # Load SSH keys into ssh-agent
|
│ ├── pos-network-ip # Show interfaces, routes, public IP + location
|
||||||
│ ├── pos-vbox # Disposable Docker-based "VMs"
|
│ ├── pos-network-scan # Parallel ping sweep of CIDR
|
||||||
│ ├── autostart.sh # Boot-time script (via systemd)
|
│ ├── pos-ssh-load-keys # Load all SSH keys into the agent
|
||||||
|
│ ├── pos-system-backup # Encrypted (AES-256) folder snapshots (tar + gpg)
|
||||||
|
│ ├── pos-system-firewall # Interactive UFW management
|
||||||
|
│ ├── pos-usb-server # USB Redirector server control (--ls, --share; prompts when args omitted)
|
||||||
|
<!-- GEN:END tree -->
|
||||||
|
│ ├── flag-reader # Inspect feature flags (list/status/--raw)
|
||||||
|
│ ├── flag-set # Set a feature flag (optionally with a value)
|
||||||
|
│ ├── flag-clear # Unset a feature flag
|
||||||
│ ├── wr-* # Legacy wrappers → pos (backward compat)
|
│ ├── wr-* # Legacy wrappers → pos (backward compat)
|
||||||
│ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos
|
│ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos
|
||||||
│ └── ssh-load-all # Legacy wrapper → pos ssh load-keys
|
│ └── ssh-load-all # Legacy wrapper → pos ssh load-keys
|
||||||
│
|
│
|
||||||
|
├── features/ # User-customizable scripts (installed via --feature)
|
||||||
|
│ └── autostart.sh # Boot-time script (via systemd, flag-gated)
|
||||||
|
│
|
||||||
|
├── templates/ # Dev-only scaffolds — NOT installed by install.sh
|
||||||
|
│ ├── pos-tool.sh # New `pos` CLI tool (→ bin/pos-<cat>-<cmd>)
|
||||||
|
│ ├── app.sh # New optional app installer (→ apps/<cat>/<name>.sh)
|
||||||
|
│ └── feature.sh # New feature script (→ features/<name>.sh)
|
||||||
|
│
|
||||||
|
├── x64_bin/ # Precompiled binaries, copied to /usr/local/bin on x86_64
|
||||||
|
│ ├── create_ap # Wi-Fi AP CLI (bash script)
|
||||||
|
│ ├── wihotspot # Wrapper → wihotspot-gui
|
||||||
|
│ └── wihotspot-gui # GTK3 hotspot GUI (x86-64 ELF)
|
||||||
|
│ # future: arm64_bin/ picked up automatically on aarch64
|
||||||
|
│
|
||||||
├── apps/ # Optional desktop app installers (by category)
|
├── apps/ # Optional desktop app installers (by category)
|
||||||
│ ├── install.sh # Interactive picker / orchestrator
|
│ ├── install.sh # Interactive picker / orchestrator
|
||||||
│ ├── browsers/
|
│ ├── browsers/
|
||||||
@@ -78,6 +100,9 @@ Linux_post_install/
|
|||||||
├── completions/
|
├── completions/
|
||||||
│ └── pos.bash # Bash tab-completion for the pos CLI
|
│ └── pos.bash # Bash tab-completion for the pos CLI
|
||||||
│
|
│
|
||||||
|
├── config/
|
||||||
|
│ └── authorized_keys # SSH public keys (gitignored)
|
||||||
|
│
|
||||||
├── compose/
|
├── compose/
|
||||||
│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)
|
│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)
|
||||||
│
|
│
|
||||||
@@ -85,8 +110,18 @@ Linux_post_install/
|
|||||||
│ ├── autostart.service # Runs autostart.sh on boot
|
│ ├── autostart.service # Runs autostart.sh on boot
|
||||||
│ └── ssh-agent.service # System-wide SSH agent socket
|
│ └── ssh-agent.service # System-wide SSH agent socket
|
||||||
│
|
│
|
||||||
├── README.md # User-facing documentation
|
├── README.md # User-facing intro + quick start (links into DOC/)
|
||||||
├── DEV.md # Developer guide
|
│
|
||||||
|
├── DOC/ # All documentation
|
||||||
|
│ ├── README.md # Docs index
|
||||||
|
│ ├── SCRIPTS.md # Installer scripts, libs, features — reference
|
||||||
|
│ ├── POS.md # pos CLI reference
|
||||||
|
│ ├── APPS.md # Optional apps reference
|
||||||
|
│ ├── SYSTEMD.md # Systemd units + completion
|
||||||
|
│ ├── DEV.md # Developer guide
|
||||||
|
│ ├── AGENT_Context_Project.md # This file — AI agent context
|
||||||
|
│ └── algorithm.md # Algorithm diagrams
|
||||||
|
│
|
||||||
├── .gitignore # Excludes secrets, Python artifacts, OS files
|
├── .gitignore # Excludes secrets, Python artifacts, OS files
|
||||||
└── .gitmodules # Submodule: compose/scale-tail → ScaleTail
|
└── .gitmodules # Submodule: compose/scale-tail → ScaleTail
|
||||||
```
|
```
|
||||||
@@ -96,20 +131,24 @@ Linux_post_install/
|
|||||||
## 3. Installation Flow
|
## 3. Installation Flow
|
||||||
|
|
||||||
```
|
```
|
||||||
User runs: ./install.sh [--apps|--full|--dry-run|--skip <phase>|--steps <spec>]
|
User runs: ./install.sh [--apps|--full|--feature|--dry-run|--skip <phase>|--steps <spec>]
|
||||||
│
|
│
|
||||||
├─ Phase 1: preinstall.sh (requires root)
|
├─ Phase 1: preinstall.sh (requires root)
|
||||||
│ └─ apt update + installs 25+ packages + yt-dlp + fail2ban
|
│ └─ apt update + installs 25+ packages + yt-dlp + fail2ban
|
||||||
│
|
│
|
||||||
├─ Phase 2: install.sh (requires root)
|
├─ Phase 2: install.sh (requires root)
|
||||||
│ └─ Copies bin/* → /usr/local/bin/ (chmod 755)
|
│ └─ Copies bin/* → /usr/local/bin/ (chmod 755)
|
||||||
│ └─ Copies lib/common.sh → /usr/local/bin/common.sh (chmod 644)
|
│ └─ Copies lib/common.sh + lib/flags.sh → /usr/local/bin/ (chmod 644)
|
||||||
|
│ └─ Copies x64_bin/* → /usr/local/bin/ on x86_64 (arm64_bin/ on aarch64)
|
||||||
|
│ └─ [if --feature] Copies features/* → /usr/local/bin/ (asks before overwriting),
|
||||||
|
│ then sets the matching feature flag
|
||||||
│
|
│
|
||||||
├─ Phase 3: postinstall.sh (runs as user)
|
├─ Phase 3: postinstall.sh (runs as user)
|
||||||
│ └─ Configures fail2ban (SSH jail: 5 retries, 1h ban)
|
│ └─ Configures fail2ban (SSH jail: 5 retries, 1h ban)
|
||||||
│ └─ PATH export in ~/.bashrc
|
│ └─ PATH export in ~/.bashrc
|
||||||
│ └─ Bash completion for pos CLI
|
│ └─ Bash completion for pos CLI
|
||||||
│ └─ Copies systemd/*.service → /etc/systemd/system/, enables them
|
│ └─ Copies systemd/*.service → /etc/systemd/system/, enables them
|
||||||
|
│ (autostart.service only when the `autostart` flag is set)
|
||||||
│
|
│
|
||||||
├─ Phase 4: ScaleTail clone
|
├─ Phase 4: ScaleTail clone
|
||||||
│ └─ Shallow-clones ScaleTail templates to /usr/local/share/linux_post_install/scale-tail
|
│ └─ Shallow-clones ScaleTail templates to /usr/local/share/linux_post_install/scale-tail
|
||||||
@@ -126,6 +165,7 @@ User runs: ./install.sh [--apps|--full|--dry-run|--skip <phase>|--steps <spec>]
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| `--apps` | Run interactive app picker after core install |
|
| `--apps` | Run interactive app picker after core install |
|
||||||
| `--full` | Core install + all apps (non-interactive) |
|
| `--full` | Core install + all apps (non-interactive) |
|
||||||
|
| `--feature` | Install `features/` scripts to `/usr/local/bin/` (asks before overwriting), set their flags |
|
||||||
| `--dry-run` | Preview without executing |
|
| `--dry-run` | Preview without executing |
|
||||||
| `--skip <phase>` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |
|
| `--skip <phase>` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |
|
||||||
| `--steps <spec>` | Run only specific phases. Format: `1,3,4` or `1-3` |
|
| `--steps <spec>` | Run only specific phases. Format: `1,3,4` or `1-3` |
|
||||||
@@ -158,28 +198,30 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
|
|||||||
|
|
||||||
| Category | Command | Script | Description |
|
| Category | Command | Script | Description |
|
||||||
|----------|---------|--------|-------------|
|
|----------|---------|--------|-------------|
|
||||||
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP |
|
<!-- GEN:START dispatch -->
|
||||||
|
| docker | compose | `pos-docker-compose` | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||||
|
| docker | health | `pos-docker-health` | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||||
|
| docker | ps | `pos-docker-ps` | Enhanced container overview (health, IPs, ports, uptime) |
|
||||||
|
| docker | vbox | `pos-docker-vbox` | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
|
||||||
|
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
||||||
|
| media | mp4 | `pos-media-mp4` | Download video as MP4 (interactive format select) |
|
||||||
| network | checkport | `pos-network-checkport` | Check TCP port connectivity |
|
| network | checkport | `pos-network-checkport` | Check TCP port connectivity |
|
||||||
|
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||||
|
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location |
|
||||||
| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |
|
| network | scan | `pos-network-scan` | Parallel ping sweep of CIDR |
|
||||||
| docker | ps | `pos-docker-ps` | Enhanced container overview |
|
| ssh | load-keys | `pos-ssh-load-keys` | Load all SSH keys into the agent |
|
||||||
| docker | health | `pos-docker-health` | Quick health dashboard (exits 1 if unhealthy) |
|
| system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||||
| docker | compose | `pos-docker-compose` | Service manager (ls/up/down/restart/logs/update/config) |
|
|
||||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 |
|
|
||||||
| media | mp4 | `pos-media-mp4` | Download video with format select |
|
|
||||||
| system | firewall | `pos-system-firewall` | Interactive UFW management |
|
| system | firewall | `pos-system-firewall` | Interactive UFW management |
|
||||||
| ssh | load-keys | `pos-ssh-load-keys` | Load SSH keys into agent |
|
| usb | server | `pos-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||||
| vbox | create | `pos-vbox create` | Create disposable VM (asks "Enter now?") |
|
<!-- GEN:END dispatch -->
|
||||||
| vbox | enter | `pos-vbox enter` | Start and exec into container |
|
|
||||||
| vbox | ls | `pos-vbox ls` | List vbox-managed containers only (label-filtered) |
|
|
||||||
| vbox | start/stop/rm | `pos-vbox start/stop/rm` | Lifecycle management |
|
|
||||||
|
|
||||||
### Legacy Wrappers
|
### Legacy Wrappers
|
||||||
|
|
||||||
These forward to `pos` transparently: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.
|
These forward to `pos` transparently: `wr-ip`, `wr-checkport`, `wr-scan-ping`, `wr-docker`, `wr-compose`, `wr-ufw`, `mp3`, `mp4`, `vbox`, `ssh-load-all`.
|
||||||
|
|
||||||
### pos vbox Details
|
### pos docker vbox Details
|
||||||
|
|
||||||
`pos-vbox` manages disposable Docker containers as lightweight VMs:
|
`pos-docker-vbox` manages disposable Docker containers as lightweight VMs:
|
||||||
|
|
||||||
- **Container labeling:** All created containers get `linux_post_install.vbox=true` label
|
- **Container labeling:** All created containers get `linux_post_install.vbox=true` label
|
||||||
- **`ls` filtering:** `docker ps --filter label=linux_post_install.vbox=true` — only shows vbox-managed containers
|
- **`ls` filtering:** `docker ps --filter label=linux_post_install.vbox=true` — only shows vbox-managed containers
|
||||||
@@ -213,7 +255,10 @@ Sourced by most scripts. Provides:
|
|||||||
source "$(dirname "$0")/../lib/common.sh"
|
source "$(dirname "$0")/../lib/common.sh"
|
||||||
```
|
```
|
||||||
|
|
||||||
**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`.
|
**Scripts that do NOT source common.sh** (self-contained):
|
||||||
|
<!-- GEN:START selfcontained -->
|
||||||
|
`bin/pos`, `pos-network-ip`, `pos-network-checkport`, `pos-network-scan`, `pos-network-hotspot`, `pos-media-mp3`, `pos-media-mp4`, `pos-ssh-load-keys`, `pos-system-firewall`, `pos-communication-telegram`.
|
||||||
|
<!-- GEN:END selfcontained -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -266,8 +311,10 @@ ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar patter
|
|||||||
|
|
||||||
- `apps/install.sh` auto-discovers all `apps/<category>/*.sh` files (excluding itself)
|
- `apps/install.sh` auto-discovers all `apps/<category>/*.sh` files (excluding itself)
|
||||||
- Three modes: interactive (default), `--all`, or specific app names as arguments
|
- 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
|
- Interactive TUI groups apps by category with section headers
|
||||||
- Each app script is standalone, idempotent, sources `lib/common.sh`
|
- Each app script is standalone, idempotent, sources `lib/common.sh`
|
||||||
|
- Every app script defines `install_<name>()` **and** `uninstall_<name>()`, dispatched via `case "${1:-}" in uninstall) ...`
|
||||||
|
|
||||||
### Installation Methods
|
### Installation Methods
|
||||||
|
|
||||||
@@ -283,7 +330,7 @@ ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar patter
|
|||||||
|
|
||||||
### Adding a New App
|
### Adding a New App
|
||||||
|
|
||||||
1. Create `apps/<name>.sh` following the template in DEV.md
|
1. Create `apps/<name>.sh` following the template in DOC/DEV.md
|
||||||
2. It auto-appears in the interactive picker — no registration needed
|
2. It auto-appears in the interactive picker — no registration needed
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -311,6 +358,15 @@ All `.service` files in `systemd/` are automatically copied to `/etc/systemd/sys
|
|||||||
- `~/.config/linux_post_install/compose.env` — Docker Compose global defaults
|
- `~/.config/linux_post_install/compose.env` — Docker Compose global defaults
|
||||||
- `~/.bashrc` — Modified by postinstall (PATH, bash completion)
|
- `~/.bashrc` — Modified by postinstall (PATH, bash completion)
|
||||||
|
|
||||||
|
### Feature Flags
|
||||||
|
|
||||||
|
System-wide flag store at `/usr/local/share/linux_post_install/flags/`:
|
||||||
|
- One file per flag; **presence = set**, **file content = optional value** (dir 755, files 644).
|
||||||
|
- Library: `lib/flags.sh` (installed as `/usr/local/bin/flags.sh`) — `flag_set <name> [value]`, `flag_clear <name>`, `flag_is_set <name>`, `flag_value <name>`, `flag_list`, `flag_status <name>`.
|
||||||
|
- CLI: `flag-reader` (list / status / `--raw`), `flag-set`, `flag-clear`.
|
||||||
|
- Set by `./install.sh --feature`; read by `postinstall.sh` to gate systemd enablement (e.g. `autostart.service` requires the `autostart` flag).
|
||||||
|
- Writes use `run` + `sudo`, so they respect `--dry-run`. `FLAGS_DIR` is env-overridable for tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Coding Conventions
|
## 10. Coding Conventions
|
||||||
@@ -348,18 +404,28 @@ All `.service` files in `systemd/` are automatically copied to `/etc/systemd/sys
|
|||||||
|
|
||||||
## 11. Development Workflow
|
## 11. Development Workflow
|
||||||
|
|
||||||
|
### Adding a New Feature
|
||||||
|
|
||||||
|
1. Create `features/<name>.sh` from `templates/feature.sh` (installed on demand via `./install.sh --feature`; never overwritten without asking)
|
||||||
|
2. `install.sh` auto-discovers it and sets its flag — no registration needed
|
||||||
|
3. If a systemd service depends on it, gate the service on `flag_is_set <name>` in `postinstall.sh`
|
||||||
|
4. Update `DOC/AGENT_Context_Project.md` file table if line counts change
|
||||||
|
|
||||||
### Adding a New App
|
### Adding a New App
|
||||||
|
|
||||||
1. Create `apps/<category>/<name>.sh` following the template in DEV.md
|
1. Create `apps/<category>/<name>.sh` from `templates/app.sh` (per DOC/DEV.md)
|
||||||
2. It auto-appears in the interactive picker — no registration needed
|
2. It auto-appears in the interactive picker — no registration needed
|
||||||
|
3. Update `DOC/APPS.md` catalog table (name, category, purpose, install method)
|
||||||
|
4. Test: `bash -n apps/<cat>/<name>.sh && shellcheck apps/<cat>/<name>.sh`
|
||||||
|
|
||||||
### Adding a New Tool
|
### Adding a New Tool
|
||||||
|
|
||||||
1. Create `bin/pos-<category>-<command>` following conventions
|
1. Create `bin/pos-<category>-<command>` from `templates/pos-tool.sh` — must be executable (`100755`); it auto-appears in `pos <category> --help` (filename-derived, no registration)
|
||||||
2. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)
|
2. Register in `bin/pos` `usage()` CATEGORIES/EXAMPLES; add to `INTERACTIVE_CMDS` in `bin/pos` if it reads stdin
|
||||||
3. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets)
|
3. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)
|
||||||
4. Update `README.md`
|
4. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets); runtime tool config → `~/.config/linux_post_install/<tool>.env` (600)
|
||||||
5. Test: `bash -n bin/your-tool && shellcheck bin/your-tool`
|
5. Update docs: `DOC/POS.md` (section table + detail), `DOC/AGENT_Context_Project.md` (bin tree, dispatch table, self-contained list, file line-count table), root `README.md` only if the category list changes
|
||||||
|
6. Test: `bash -n bin/your-tool && shellcheck bin/your-tool && bin/pos help <full command> && bin/pos <category> --help`
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
@@ -391,18 +457,32 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
|
|
||||||
| File | Lines | Purpose |
|
| File | Lines | Purpose |
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |
|
| `install.sh` | 192 | Main orchestrator — 4 phases with CLI flags, `--feature`, prebuilt arch bins |
|
||||||
| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |
|
| `preinstall.sh` | 54 | System packages + hotspot deps + yt-dlp + fail2ban |
|
||||||
| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |
|
| `postinstall.sh` | 97 | fail2ban config, PATH, bash completion, systemd (flag-gated) |
|
||||||
| `lib/common.sh` | 121 | Shared library |
|
| `lib/common.sh` | 121 | Shared library |
|
||||||
| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |
|
| `lib/flags.sh` | 60 | Feature flag store (set/clear/is_set/value/list/status) |
|
||||||
| `bin/pos-docker-compose` | 317 | Largest script — full compose management |
|
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
|
||||||
| `bin/pos-system-firewall` | 284 | Interactive UFW manager |
|
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
|
||||||
| `bin/pos-docker-ps` | 127 | Enhanced container overview |
|
| `bin/flag-clear` | 21 | Unset a flag |
|
||||||
| `bin/pos-docker-health` | ~90 | Quick health dashboard |
|
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
|
||||||
| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |
|
| `bin/pos-communication-telegram` | 140 | Send Telegram messages via Bot API (--send, test, config set) |
|
||||||
| `completions/pos.bash` | 118 | Dynamic bash completion |
|
| `bin/pos-docker-compose` | 364 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
|
||||||
| `apps/install.sh` | 99 | App picker/orchestrator |
|
| `bin/pos-docker-health` | 110 | One-glance container health dashboard (exits 1 if unhealthy) |
|
||||||
|
| `bin/pos-docker-ps` | 128 | Enhanced container overview (health, IPs, ports, uptime) |
|
||||||
|
| `bin/pos-docker-vbox` | 157 | Disposable Docker-based VMs (create/enter/start/stop/rm/ls) |
|
||||||
|
| `bin/pos-media-mp3` | 31 | Download audio as MP3 (yt-dlp) |
|
||||||
|
| `bin/pos-media-mp4` | 34 | Download video as MP4 (interactive format select) |
|
||||||
|
| `bin/pos-network-checkport` | 45 | Check TCP port connectivity |
|
||||||
|
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||||
|
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
|
||||||
|
| `bin/pos-network-scan` | 271 | Parallel ping sweep of CIDR |
|
||||||
|
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
|
||||||
|
| `bin/pos-system-backup` | 117 | Encrypted (AES-256) folder snapshots (tar + gpg) |
|
||||||
|
| `bin/pos-system-firewall` | 285 | Interactive UFW management |
|
||||||
|
| `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
|
||||||
|
| `completions/pos.bash` | 146 | Dynamic bash completion |
|
||||||
|
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -410,17 +490,22 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
|||||||
|
|
||||||
| Task | Where to Edit |
|
| Task | Where to Edit |
|
||||||
|------|---------------|
|
|------|---------------|
|
||||||
| Add a new CLI tool | Create `bin/pos-<cat>-<cmd>`, add deps in `preinstall.sh` |
|
| Add a new CLI tool | Create `bin/pos-<cat>-<cmd>`, add apt deps in `preinstall.sh` (non-apt/manual installers: add a `command -v` guard in the tool instead) |
|
||||||
| Add a new app installer | Create `apps/<name>.sh` (auto-discovered) |
|
| Add a new app installer | Create `apps/<name>.sh` (auto-discovered) |
|
||||||
| Add a systemd service | Create `systemd/<name>.service` (auto-installed by postinstall) |
|
| Add a feature | Create `features/<name>.sh` (installed on demand via `./install.sh --feature`) |
|
||||||
|
| Add a systemd service | Create `systemd/<name>.service` (auto-installed by postinstall; gate on a flag if it backs a feature) |
|
||||||
|
| Add a precompiled binary | Drop it in `x64_bin/` (or `arm64_bin/` later) — auto-copied by Phase 2 |
|
||||||
|
| Inspect/set feature flags | `flag-reader`, `flag-set`, `flag-clear` (or source `lib/flags.sh`) |
|
||||||
| Modify package list | Edit `PACKAGES` array in `preinstall.sh` |
|
| Modify package list | Edit `PACKAGES` array in `preinstall.sh` |
|
||||||
| Change PATH or bash config | Edit `postinstall.sh` |
|
| Change PATH or bash config | Edit `postinstall.sh` |
|
||||||
| Modify fail2ban config | Edit jail.local section in `postinstall.sh` |
|
| Modify fail2ban config | Edit jail.local section in `postinstall.sh` |
|
||||||
| Add bash completion | Edit `completions/pos.bash` |
|
| Add bash completion | Edit `completions/pos.bash` |
|
||||||
| Modify Docker Compose logic | Edit `bin/pos-docker-compose` |
|
| Modify Docker Compose logic | Edit `bin/pos-docker-compose` |
|
||||||
| Modify Docker health check | Edit `bin/pos-docker-health` |
|
| Modify Docker health check | Edit `bin/pos-docker-health` |
|
||||||
|
| Modify vbox (Docker VM) logic | Edit `bin/pos-docker-vbox` |
|
||||||
|
| Modify USB forwarding logic | Edit `bin/pos-usb-server` |
|
||||||
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
|
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
|
||||||
| Modify pos logging | Edit log setup in `bin/pos` |
|
| Modify pos logging | Edit log setup in `bin/pos` |
|
||||||
| Modify install phases/flags | Edit arg parsing in `install.sh` |
|
| Modify install phases/flags | Edit arg parsing in `install.sh` |
|
||||||
| Update documentation | Edit `README.md` and/or `DEV.md` |
|
| Update documentation | Edit the relevant doc under `DOC/` (index: `DOC/README.md`) |
|
||||||
| Add a secret config file | Add to `config/`, update `.gitignore`, add copy logic in `postinstall.sh` |
|
| Add a secret config file | Add to `config/`, update `.gitignore`, add copy logic in `postinstall.sh` |
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
# Optional Apps Reference
|
||||||
|
|
||||||
|
`apps/` holds 15 optional desktop application installers, one script per app in `apps/<category>/<name>.sh`. They are **not** installed by the core bootstrap — run the picker explicitly.
|
||||||
|
|
||||||
|
- [The picker — `apps/install.sh`](#the-picker--appsinstallsh)
|
||||||
|
- [How an app installer works](#how-an-app-installer-works)
|
||||||
|
- [App catalog](#app-catalog)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The picker — `apps/install.sh`
|
||||||
|
|
||||||
|
**Purpose:** discover every app under `apps/` and install/uninstall the selection. Apps are auto-discovered from the directory structure — no registration step.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash apps/install.sh # interactive install selection
|
||||||
|
bash apps/install.sh --all # install everything
|
||||||
|
bash apps/install.sh brave vscode # install specific apps
|
||||||
|
bash apps/install.sh --uninstall # interactive uninstall selection
|
||||||
|
bash apps/install.sh --uninstall --all # uninstall everything
|
||||||
|
bash apps/install.sh --uninstall brave # uninstall a specific app
|
||||||
|
```
|
||||||
|
|
||||||
|
(Also reachable via `./install.sh --apps` / `--full`.)
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. Parses `--all`, `--uninstall`, and positional app names.
|
||||||
|
2. Scans `apps/<category>/*.sh` to build the catalog (skips non-app dirs).
|
||||||
|
3. Picks apps three ways: named on the command line (unknown names are skipped with a warning), `--all`, or an interactive y/n picker grouped by category.
|
||||||
|
4. Runs `bash apps/<category>/<name>.sh [uninstall]` for each selected app, with a progress header and an overall elapsed-time banner.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- Categories: `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`.
|
||||||
|
- Adding an app = dropping `apps/<category>/<name>.sh` into the folder. See [DEV.md](DEV.md) for the required installer conventions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How an app installer works
|
||||||
|
|
||||||
|
Every app script follows the same shape:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install_<name>() { … } # idempotent: checks command -v (or flatpak list) first
|
||||||
|
uninstall_<name>() { … } # also idempotent; purges and removes any added repos/keys
|
||||||
|
case "${1:-}" in
|
||||||
|
uninstall) uninstall_<name> ;;
|
||||||
|
*) install_<name> ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
Installation methods used across the catalog:
|
||||||
|
|
||||||
|
| Method | Example |
|
||||||
|
|--------|---------|
|
||||||
|
| `apt` package | `sudo apt install -y obs-studio` |
|
||||||
|
| Official installer script | `curl -fsSL https://tailscale.com/install.sh \| sh` |
|
||||||
|
| Custom apt repo (added at install, removed at uninstall) | Brave, VS Code |
|
||||||
|
| `.deb` file | `curl` → `dpkg -i` → `apt-get install -f -y` |
|
||||||
|
| GitHub release archive | scrcpy (tar.gz → `/usr/local/lib/`) |
|
||||||
|
| AppImage | AFFiNE (`/opt/affine` + desktop entry) |
|
||||||
|
| Flatpak | LocalSend (`flatpak install -y flathub …`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## App catalog
|
||||||
|
|
||||||
|
| App | Category | What it is | Install method |
|
||||||
|
|-----|----------|------------|----------------|
|
||||||
|
| Brave | browsers | Brave browser | apt repo + `apt install brave-browser` |
|
||||||
|
| opencode | development | AI coding agent | official script → `~/.opencode/bin` |
|
||||||
|
| VS Code | development | Code editor | Microsoft apt repo + `apt install code` |
|
||||||
|
| OBS Studio | media | Screen recording / streaming | `apt install obs-studio` |
|
||||||
|
| scrcpy | media | Android mirror/control | GitHub release (latest) → `/usr/local/lib/scrcpy-<v>` + desktop entry |
|
||||||
|
| VLC | media | Media player | `apt install vlc` |
|
||||||
|
| NetBird | networking | Mesh VPN | official script; join with `sudo netbird up --setup-key <key>` |
|
||||||
|
| Tailscale | networking | WireGuard-based VPN | official script; start with `sudo tailscale up` |
|
||||||
|
| ZeroTier | networking | Virtual LAN | official script; join with `sudo zerotier-cli join <id>` |
|
||||||
|
| Termius | remote-access | SSH client | `.deb` from termius.com |
|
||||||
|
| VNC Viewer | remote-access | VNC client (TigerVNC) | `apt install tigervnc-viewer` |
|
||||||
|
| Docker Engine | system | Container runtime | get.docker.com; adds user to `docker` group (re-login needed) |
|
||||||
|
| QEMU + KVM | system | Virtualization + virt-manager | `apt install` (qemu-system, libvirt, bridge-utils, virt-manager); adds user to `libvirt`/`kvm` groups |
|
||||||
|
| AFFiNE | utilities | Knowledge base (AppImage) | GitHub release → `/opt/affine` + desktop entry |
|
||||||
|
| btop | utilities | Resource monitor | `apt install btop` |
|
||||||
|
| LocalSend | utilities | Local file sharing | flatpak (installs flatpak + flathub if missing) |
|
||||||
+99
-13
@@ -49,6 +49,13 @@ pos docker compose up jellyfin
|
|||||||
|
|
||||||
All non-interactive commands log to `~/.local/share/linux_post_install/logs/`.
|
All non-interactive commands log to `~/.local/share/linux_post_install/logs/`.
|
||||||
|
|
||||||
|
`pos help <full command>` shows a tool's help, e.g. `pos help communication telegram` (all words joined with dashes → `pos-communication-telegram --help`). `pos <category>` or `pos <category> --help` shows a category's subcommands (derived from the `pos-<category>-*` filenames in `bin/` — no script execution, so it works even for root-only/interactive tools like `system-firewall`).
|
||||||
|
|
||||||
|
**When adding a command, `bin/pos` itself has two things to keep in sync:**
|
||||||
|
|
||||||
|
- **The usage text** (`usage()` function) — the CATEGORIES and EXAMPLES blocks are the built-in cheat-sheet (`pos --help`). Add the new command there or it stays invisible.
|
||||||
|
- **`INTERACTIVE_CMDS`** (space-separated list above the dispatch loop) — commands that **read stdin** (password prompts, selection menus: `media-mp4`, `system-backup`, `usb-server`) must be added here. Everything else is piped through `tee` for logging, which would hang or swallow an interactive prompt. sudo's own password prompt is unaffected — it reads from `/dev/tty`. Trade-off: it's all-or-nothing **per script** — adding a flag-style tool with *any* prompting subcommand (e.g. `usb-server --share`) means *every* subcommand of that script skips output logging (e.g. `usb server --ls` loses the `tee` log too).
|
||||||
|
|
||||||
### Shared Library (`lib/common.sh`)
|
### Shared Library (`lib/common.sh`)
|
||||||
|
|
||||||
Sourced by most scripts. Key functions:
|
Sourced by most scripts. Key functions:
|
||||||
@@ -70,6 +77,8 @@ Sourced by most scripts. Key functions:
|
|||||||
|
|
||||||
## Adding a New CLI Tool
|
## Adding a New CLI Tool
|
||||||
|
|
||||||
|
Start from the template: `cp templates/pos-tool.sh bin/pos-<category>-<command> && chmod +x bin/pos-<category>-<command>`.
|
||||||
|
|
||||||
### 1. Create the script
|
### 1. Create the script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -105,8 +114,17 @@ esac
|
|||||||
warn() { echo "[!] $*"; }
|
warn() { echo "[!] $*"; }
|
||||||
err() { echo "ERROR: $*" >&2; exit 1; }
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
```
|
```
|
||||||
|
If you skip `common.sh`, add the tool to the "Scripts that do NOT source common.sh" list in `DOC/AGENT_Context_Project.md`.
|
||||||
|
|
||||||
### 2. Add system dependencies
|
### 2. Make it discoverable
|
||||||
|
|
||||||
|
- The dispatcher auto-discovers executable `bin/pos-*` files — no registration needed. The file **must be executable** (`chmod +x`, committed as mode `100755`); the dispatcher and `install.sh` skip non-executables.
|
||||||
|
- `pos <category> --help` (and bare `pos <category>`) is derived from the `pos-<category>-*` filenames too — a new tool appears in its category's help automatically, with no registration (see [The `pos` CLI](#the-pos-cli)).
|
||||||
|
- Add the command to the `usage()` CATEGORIES/EXAMPLES blocks in `bin/pos` (see [The `pos` CLI](#the-pos-cli)).
|
||||||
|
- If the command **reads stdin** (prompts/selection), add it to `INTERACTIVE_CMDS` in `bin/pos` — see [The `pos` CLI](#the-pos-cli).
|
||||||
|
- If it takes flag-style args (e.g. `--send "text"`), consider extending `completions/pos.bash`; category/subcommand names are auto-discovered from the filename.
|
||||||
|
|
||||||
|
### 3. Add system dependencies
|
||||||
|
|
||||||
Add package names to the `PACKAGES` array in `preinstall.sh`:
|
Add package names to the `PACKAGES` array in `preinstall.sh`:
|
||||||
|
|
||||||
@@ -117,30 +135,42 @@ PACKAGES=(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Add config files (if needed)
|
**Not in apt?** If the dependency ships as a manual installer (no package — e.g. `usbsrv`, the USB Redirector server), do **not** put it in `PACKAGES` (that would break `preinstall.sh`). Instead, add a `command -v <binary> || err "… install from <URL>"` guard in the tool itself and note the manual install in `usage()`/`DOC/POS.md`.
|
||||||
|
|
||||||
Place defaults in `config/` and add copy logic to `postinstall.sh`. If they contain secrets, add to `.gitignore` and document in README.
|
### 4. Config files (if needed)
|
||||||
|
|
||||||
### 4. Add SSH keys (if needed)
|
Two kinds of config, don't mix them up:
|
||||||
|
|
||||||
|
- **Machine defaults shipped by the installer:** place the file in `config/` and add copy logic to `postinstall.sh`. If it contains secrets, add to `.gitignore` and document in `DOC/`.
|
||||||
|
- **Runtime tool config set by the user:** `~/.config/linux_post_install/<tool>.env` with `chmod 600`. Load it with env-var precedence (flags > environment > file). Patterns: `pos-docker-compose` (`compose.env`) and `pos-communication-telegram` (`telegram.env`, token masked in `config` output). Never store tokens in the repo.
|
||||||
|
|
||||||
|
### 5. Add SSH keys (if needed)
|
||||||
|
|
||||||
Place public keys in `config/authorized_keys` (one per line). `postinstall.sh` reads this file automatically.
|
Place public keys in `config/authorized_keys` (one per line). `postinstall.sh` reads this file automatically.
|
||||||
|
|
||||||
### 5. Update README
|
### 6. Update the docs
|
||||||
|
|
||||||
Add a section under the relevant category in README.md.
|
- `DOC/POS.md`: add the command to the section table + a detail block (commands, behavior, configuration).
|
||||||
|
- `DOC/AGENT_Context_Project.md`: update the bin tree, the dispatch table, the "scripts that do NOT source common.sh" list (if applicable), and the file line-count table.
|
||||||
|
- Root `README.md`: only if the `pos` category list in the help text changes.
|
||||||
|
|
||||||
### 6. Test
|
### 7. Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
chmod +x bin/your-tool
|
||||||
bash -n bin/your-tool
|
bash -n bin/your-tool
|
||||||
shellcheck bin/your-tool
|
shellcheck bin/your-tool
|
||||||
./bin/your-tool --help
|
./bin/your-tool --help
|
||||||
|
bin/pos help <full command> # confirm dispatch works
|
||||||
|
bin/pos <category> --help # confirm category listing includes the new tool (first tool in a new category)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Adding an Optional App
|
## Adding an Optional App
|
||||||
|
|
||||||
|
Start from the template: `cp templates/app.sh apps/<category>/<name>.sh`.
|
||||||
|
|
||||||
### 1. Create the installer
|
### 1. Create the installer
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -153,20 +183,34 @@ install_myapp() {
|
|||||||
spawn "Installing myapp" sudo apt install -y 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/<category>/<name>.sh`. It auto-appears in the picker — no registration needed.
|
Place it in `apps/<category>/<name>.sh`. It auto-appears in the picker — no registration needed.
|
||||||
|
|
||||||
**Categories:** `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`
|
**Categories:** `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`
|
||||||
|
|
||||||
|
**Docs:** add a row to the catalog table in `DOC/APPS.md` (name, category, purpose, install method).
|
||||||
|
|
||||||
### 2. Conventions
|
### 2. Conventions
|
||||||
|
|
||||||
- Idempotent: check `command -v` before installing
|
- Idempotent: check `command -v` (or `flatpak list` / file existence) before installing **and** uninstalling
|
||||||
- APT packages → `sudo apt install -y` inside `spawn`
|
- Every app **must** provide an `uninstall_<name>()` 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`
|
- Official scripts → `curl ... | sh` inside `spawn`
|
||||||
- Flatpak → `flatpak install -y flathub <app-id>` inside `spawn`
|
- Flatpak → `flatpak install -y flathub <app-id>` inside `spawn`, remove with `flatpak uninstall -y <app-id>`
|
||||||
- `.deb` files → download to temp, `sudo apt install -y ./file.deb` inside `spawn`
|
- `.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
|
- `usermod` for groups → print re-login reminder
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -176,7 +220,7 @@ Place it in `apps/<category>/<name>.sh`. It auto-appears in the picker — no re
|
|||||||
1. Find the script in `bin/`
|
1. Find the script in `bin/`
|
||||||
2. Understand its contract (args, output, exit codes)
|
2. Understand its contract (args, output, exit codes)
|
||||||
3. Make the change — keep it idempotent
|
3. Make the change — keep it idempotent
|
||||||
4. Update README if behaviour changed
|
4. Update `DOC/POS.md` (or the relevant doc) if behaviour changed
|
||||||
5. Run `shellcheck` on the modified file
|
5. Run `shellcheck` on the modified file
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -216,8 +260,9 @@ run sudo apt install -y git
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Never hardcode secrets — put them in `config/` (gitignored)
|
- Never hardcode secrets — put them in `config/` (gitignored) or, for runtime tool config, `~/.config/linux_post_install/<tool>.env`
|
||||||
- `chmod 600` for sensitive files
|
- `chmod 600` for sensitive files
|
||||||
|
- Mask secrets in `config` output (see `pos-communication-telegram`'s `mask_token`)
|
||||||
- Validate input before shell commands
|
- Validate input before shell commands
|
||||||
- Use `sudo` only where needed
|
- Use `sudo` only where needed
|
||||||
|
|
||||||
@@ -226,10 +271,51 @@ run sudo apt install -y git
|
|||||||
- CLI tools: `bin/pos-<category>-<command>`
|
- CLI tools: `bin/pos-<category>-<command>`
|
||||||
- Legacy wrappers: `bin/wr-*`
|
- Legacy wrappers: `bin/wr-*`
|
||||||
- App installers: `apps/<category>/<name>.sh`
|
- App installers: `apps/<category>/<name>.sh`
|
||||||
|
- Features: `features/<name>.sh`
|
||||||
- Lowercase with hyphens
|
- Lowercase with hyphens
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Features & Flags
|
||||||
|
|
||||||
|
`features/` holds scripts the user is likely to customize (e.g. `autostart.sh`). Unlike `bin/` (synced on every install), features are installed on demand and **never overwritten without asking**.
|
||||||
|
|
||||||
|
### Adding a Feature
|
||||||
|
|
||||||
|
Start from the template: `cp templates/feature.sh features/<name>.sh`.
|
||||||
|
|
||||||
|
1. Create `features/<name>.sh` following the CLI tool template (shebang, `set -euo pipefail`, `--help`).
|
||||||
|
2. Nothing else is registered — `./install.sh --feature` auto-discovers it, copies it to `/usr/local/bin/`, asks before overwriting an existing file, and sets its flag.
|
||||||
|
3. If the feature backs a systemd service, gate the service on the flag in `postinstall.sh` (see below).
|
||||||
|
|
||||||
|
### Flag System
|
||||||
|
|
||||||
|
System-wide flag store at `/usr/local/share/linux_post_install/flags/` (presence = set, content = optional value). Sourced via `lib/flags.sh` (or the installed `/usr/local/bin/flags.sh`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source "$(dirname "$0")/lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||||
|
|
||||||
|
flag_set autostart # green flag
|
||||||
|
flag_set app "2.1" # green flag with a value
|
||||||
|
flag_is_set autostart # test (0/1) — the primitive consumers use
|
||||||
|
flag_value app # → "2.1"
|
||||||
|
flag_list # names of all set flags
|
||||||
|
flag_clear autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes use `run` + `sudo`, so they respect `--dry-run`. CLI equivalents: `flag-reader`, `flag-set`, `flag-clear`.
|
||||||
|
|
||||||
|
**Example — service gated on a flag** (in `postinstall.sh`'s systemd loop):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ "$svc_name" = "myapp.service" ] && ! flag_is_set myapp; then
|
||||||
|
warn "myapp feature not installed — skipping myapp.service"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Working with Systemd
|
## Working with Systemd
|
||||||
|
|
||||||
Create `systemd/<name>.service` — `postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically.
|
Create `systemd/<name>.service` — `postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically.
|
||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
# `pos` CLI Reference
|
||||||
|
|
||||||
|
`pos` is the unified command-line interface installed to `/usr/local/bin/`. Every tool is a small script in `bin/` with a `pos-<category>-<command>` name. This document explains the dispatcher and every command.
|
||||||
|
|
||||||
|
- [The dispatcher — `bin/pos`](#the-dispatcher--binpos)
|
||||||
|
- [Logging behavior](#logging-behavior)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [network](#network)
|
||||||
|
- [docker](#docker)
|
||||||
|
- [media](#media)
|
||||||
|
- [system](#system)
|
||||||
|
- [ssh](#ssh)
|
||||||
|
- [usb](#usb)
|
||||||
|
- [communication](#communication)
|
||||||
|
- [flags](#flags)
|
||||||
|
- [Legacy wrappers](#legacy-wrappers)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The dispatcher — `bin/pos`
|
||||||
|
|
||||||
|
**Purpose:** turn `pos <category> <command> [args]` into a call to the matching `pos-*` script.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
`pos` scans its own directory for executable `pos-*` files and tries **variable-length argument matching**, longest first. For `pos docker compose up jellyfin`:
|
||||||
|
|
||||||
|
```
|
||||||
|
tries pos-docker-compose-up-jellyfin (not found)
|
||||||
|
tries pos-docker-compose-up (not found)
|
||||||
|
tries pos-docker-compose (found) → runs with args "up jellyfin"
|
||||||
|
```
|
||||||
|
|
||||||
|
`pos help <full command>` runs that tool's `--help` (e.g. `pos help communication telegram`, `pos help docker compose` — the words are joined with dashes). `pos <category>` and `pos <category> --help` list that category's subcommands (derived from `bin/pos-<category>-*` filenames, no script execution). Running `pos` with no args prints the built-in usage text (which doubles as the category cheat-sheet).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging behavior
|
||||||
|
|
||||||
|
Every non-interactive `pos` invocation logs to `~/.local/share/linux_post_install/logs/`:
|
||||||
|
|
||||||
|
- Per-command files: `YYYYMMDD_HHMMSS_pos_<args>.log` (full stdout + stderr).
|
||||||
|
- `pos.log`: one line per invocation — command, log file, exit code.
|
||||||
|
- **Interactive** commands (`pos system firewall`, `pos media mp4`, `pos system backup`) only log the invocation, not their output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### network
|
||||||
|
|
||||||
|
| Command | File | Purpose | Configuration |
|
||||||
|
|---------|------|---------|---------------|
|
||||||
|
| `pos network ip` | `bin/pos-network-ip` | Show interfaces, default route, public IP + location | None. Public IP via `https://ifconfig.me`; location via `ip-api.com` (5s timeouts) |
|
||||||
|
| `pos network checkport <ip:port>` | `bin/pos-network-checkport` | Check if a TCP port is open | None. Uses `/dev/tcp` with a 2s timeout; exit 0/1 via OPEN/CLOSED |
|
||||||
|
| `pos network scan <cidr> [--full] [--retries N]` | `bin/pos-network-scan` | Two-phase nmap scan | See below |
|
||||||
|
| `pos network hotspot [cmd]` | `bin/pos-network-hotspot` | Wi-Fi hotspot via `create_ap` (CLI) or `wihotspot-gui` (GUI) | Uses the precompiled binaries from `x64_bin/`; see below |
|
||||||
|
|
||||||
|
**`pos network scan` in detail:**
|
||||||
|
|
||||||
|
- Phase 1 — fast host discovery (`nmap -sn -T5`), prints the live host list.
|
||||||
|
- Phase 2 (only with `--full`) — service/version scan (`-sV -sC`), plus OS detection and NSE scripts if run with privileges; shows ports, OS, SSH host keys, HTTP titles, NetBIOS/SMB info.
|
||||||
|
- Accepts a bare IP (treated as `/32`) or a CIDR.
|
||||||
|
- Auto-raises to `sudo nmap` when possible (root, passwordless sudo, or an interactive terminal with `--full`).
|
||||||
|
- `--retries N` tunes discovery retries (default 1).
|
||||||
|
|
||||||
|
**`pos network hotspot` in detail:**
|
||||||
|
|
||||||
|
Backed by the precompiled binaries shipped in `x64_bin/` (see [SCRIPTS.md → x64_bin/](SCRIPTS.md#x64_bin--precompiled-binaries)). Needs root for the CLI commands (uses `sudo`):
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `pos network hotspot` | Launches the `wihotspot-gui` (GTK3 GUI) |
|
||||||
|
| `pos network hotspot start <wifi-iface> [<internet-iface>] <ssid> [<passphrase>]` | Asks whether to run in the background; `y` starts `create_ap --daemon` (logs to `/var/log/linux_post_install_hotspot.log`), `n` runs in the foreground (blocks until Ctrl+C) |
|
||||||
|
| `pos network hotspot start --foreground <wifi-iface> [<internet-iface>] <ssid> [<passphrase>]` | Skips the prompt, runs in the foreground |
|
||||||
|
| `pos network hotspot stop [<id>]` | Stops the running access point via `create_ap --stop`; `<id>` is an interface name or PID, auto-detected if omitted |
|
||||||
|
| `pos network hotspot status` | Runs `create_ap --list-running` |
|
||||||
|
|
||||||
|
### docker
|
||||||
|
|
||||||
|
| Command | File | Purpose | Configuration |
|
||||||
|
|---------|------|---------|---------------|
|
||||||
|
| `pos docker ps` | `bin/pos-docker-ps` | Enhanced container list: name, image, health, uptime, IPs, ports, ID, plus a healthy/unhealthy summary | None. Requires Docker + Python 3 |
|
||||||
|
| `pos docker health` | `bin/pos-docker-health` | One-glance health dashboard; **exits 1** if any container is unhealthy | None. Checks all containers including stopped ones |
|
||||||
|
| `pos docker compose …` | `bin/pos-docker-compose` | ScaleTail service manager | See [Docker Compose / ScaleTail](#docker-compose--scaletail) below |
|
||||||
|
|
||||||
|
#### Docker Compose / ScaleTail
|
||||||
|
|
||||||
|
**`pos docker compose ls`** — list available ScaleTail service templates.
|
||||||
|
|
||||||
|
**`pos docker compose installed`** — list deployed services under `$SERVICES_BASE`.
|
||||||
|
|
||||||
|
**`pos docker compose up <service>`** — deploy a service:
|
||||||
|
|
||||||
|
1. If not yet deployed, creates `$SERVICES_BASE/<service>/` with `config/` and `data/`, copies the template's `compose.yaml`.
|
||||||
|
2. If no `.env` exists, copies the template's `.env` (or writes a default) and fills in your global config values (`TS_AUTHKEY`, `TZ`, `DNS_SERVER`).
|
||||||
|
3. If `TS_AUTHKEY` is still empty, prompts for it.
|
||||||
|
4. Offers to edit `.env` before starting (default **yes** on first deploy).
|
||||||
|
5. Runs `docker compose up -d`.
|
||||||
|
|
||||||
|
**`pos docker compose down/restart/logs <service>`** — stop, restart, or tail logs of a deployment.
|
||||||
|
|
||||||
|
**`pos docker compose update`** — `git pull` the ScaleTail templates, then refresh the `compose.yaml` of every deployed service. **Per-service `.env` files are never touched.**
|
||||||
|
|
||||||
|
**`pos docker compose config [show]`** — show the global config file and `SERVICES_BASE`.
|
||||||
|
|
||||||
|
**`pos docker compose config set KEY=VALUE`** — set/update a global default in `~/.config/linux_post_install/compose.env`.
|
||||||
|
|
||||||
|
**`pos docker compose config edit`** — open the global config in `$EDITOR` (creates a default file first).
|
||||||
|
|
||||||
|
Configuration (three layers, most specific wins):
|
||||||
|
|
||||||
|
| Layer | File | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| Template defaults | `/usr/local/share/linux_post_install/scale-tail/services/<name>/.env` | Read-only |
|
||||||
|
| Global config | `~/.config/linux_post_install/compose.env` | Edited via `config set` / `config edit` |
|
||||||
|
| Per-service | `$SERVICES_BASE/<service>/.env` | Created on first `up`, **never overwritten** |
|
||||||
|
|
||||||
|
Global config keys:
|
||||||
|
|
||||||
|
| Key | Required | Default | Purpose |
|
||||||
|
|-----|----------|---------|---------|
|
||||||
|
| `TS_AUTHKEY` | yes | — | Tailscale auth key for the sidecar |
|
||||||
|
| `TZ` | no | `Europe/Amsterdam` | Service timezone |
|
||||||
|
| `DNS_SERVER` | no | `9.9.9.9` | DNS server |
|
||||||
|
| `SERVICES_BASE` | no | `/srv` | Deployment root |
|
||||||
|
|
||||||
|
#### Docker vbox
|
||||||
|
|
||||||
|
**File:** `bin/pos-docker-vbox`
|
||||||
|
**Purpose:** manage disposable Docker containers as lightweight "VMs". Each container gets a bind-mounted host directory so files persist after the container is removed. Containers carry the label `linux_post_install.vbox=true`.
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `pos docker vbox create <name> [image] [--dir <path>]` | Creates a container from `ubuntu:22.04` (or the given image), bind-mounting `~/<name>` (or `--dir`, or `.` for cwd) as the working directory; prompts to enter immediately |
|
||||||
|
| `pos docker vbox enter <name>` | Shell into the container (auto-starts it if stopped); detects the working dir from the container mounts |
|
||||||
|
| `pos docker vbox start/stop/rm <name>` | Start, stop, or force-remove the container |
|
||||||
|
| `pos docker vbox ls` | List vbox containers only (label filter) |
|
||||||
|
|
||||||
|
The standalone `vbox` command still works and forwards to `pos docker vbox` (see [Legacy wrappers](#legacy-wrappers)).
|
||||||
|
|
||||||
|
### media
|
||||||
|
|
||||||
|
| Command | File | Purpose | Configuration |
|
||||||
|
|---------|------|---------|---------------|
|
||||||
|
| `pos media mp3 <url>` | `bin/pos-media-mp3` | Download audio as MP3 via yt-dlp, with thumbnail + metadata | Output to `~/Music/%(title)s.%(ext)s`, `--audio-quality 0` |
|
||||||
|
| `pos media mp4 <url>` | `bin/pos-media-mp4` | Download video via yt-dlp with **interactive format selection** | Lists formats (`yt-dlp -F`), asks for a format ID, saves to `~/Videos/` |
|
||||||
|
|
||||||
|
### system
|
||||||
|
|
||||||
|
| Command | File | Purpose | Configuration |
|
||||||
|
|---------|------|---------|---------------|
|
||||||
|
| `sudo pos system firewall` | `bin/pos-system-firewall` | Interactive UFW ("UFW POWER") menu: add/delete rules, status, enable/disable/reset, default policies | Must run as root. Every command is previewed and confirmed before execution; supports `--dry-run`; keeps a history of executed commands |
|
||||||
|
| `pos system backup <folder-path>` | `bin/pos-system-backup` | Create a gpg-encrypted (AES-256) `tar.gz` snapshot of a folder and verify it | Prompts twice for a password (never stored). Uses `sudo tar`; needs `gnupg` (in `preinstall.sh` PACKAGES). Artifact `<name>_<date>.tar.gz.gpg` in the current directory, `chmod 600` |
|
||||||
|
| `pos system backup --service` | `bin/pos-system-backup` | Lists folders under `/srv` and `~/srv`, lets you pick one, then runs the same backup | Roots via `BACKUP_SERVICE_ROOTS` (space-separated, default `/srv $HOME/srv`) |
|
||||||
|
|
||||||
|
### ssh
|
||||||
|
|
||||||
|
| Command | File | Purpose | Configuration |
|
||||||
|
|---------|------|---------|---------------|
|
||||||
|
| `pos ssh load-keys` | `bin/pos-ssh-load-keys` | Load all `~/.ssh/id_*` private keys into the ssh-agent | Uses `SSH_AUTH_SOCK` (default `/run/ssh-agent/socket`, provided by `ssh-agent.service`); skips `.pub`, `known_hosts`, `authorized_keys`, `config`; validates keys before adding |
|
||||||
|
|
||||||
|
### usb
|
||||||
|
|
||||||
|
**File:** `bin/pos-usb-server`
|
||||||
|
**Purpose:** control the USB Redirector server (`usbsrv`) — share local USB devices over the network and manage connected clients. Requires `usbsrv` (manual install from incentivespro.com — not in `PACKAGES`).
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `pos usb server --ls` | List host USB devices and connected clients |
|
||||||
|
| `pos usb server --ls-shared` | List shared or in-use devices only |
|
||||||
|
| `pos usb server --share [dev-id] [client-id]` | Share a device and connect it to a client; interactive picker when IDs are omitted (`-share` + `-connect-to CLIENT-DEV`) |
|
||||||
|
| `pos usb server --unshare [dev-id]` | Stop sharing a device |
|
||||||
|
| `pos usb server --auto-share on\|off` | Toggle automatic sharing of new devices |
|
||||||
|
| `pos usb server --callback [addr:port]` | Create a callback connection to a client |
|
||||||
|
| `pos usb server --close-callback [target\|all]` | Close a client callback |
|
||||||
|
| `pos usb server --auto-connect on\|off [client]` | Toggle remote auto-connect for a client |
|
||||||
|
| `pos usb server --disconnect [dev-id\|all]` | Disconnect a device from its clients |
|
||||||
|
| `pos usb server --nickname [dev-id] [nick]` | Set a device nickname (empty nick removes it) |
|
||||||
|
| `pos usb server --timeout [dev-id] [sec]` | Set device inactivity timeout (0 disables) |
|
||||||
|
| `pos usb server --port [num]` | Set the TCP port (restart server to apply) |
|
||||||
|
| `pos usb server --info` / `--version` | Show server info / version |
|
||||||
|
|
||||||
|
Subcommands that need input prompt interactively when args are omitted.
|
||||||
|
|
||||||
|
### communication
|
||||||
|
|
||||||
|
| Command | File | Purpose | Configuration |
|
||||||
|
|---------|------|---------|---------------|
|
||||||
|
| `pos communication telegram --send "text"` | `bin/pos-communication-telegram` | Send a text message to a Telegram chat via the Bot API | Token + chat ID from `~/.config/linux_post_install/telegram.env` (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, chmod 600). Precedence: `--token`/`--chat-id` flags > env > config file |
|
||||||
|
|
||||||
|
`pos communication telegram` in detail:
|
||||||
|
|
||||||
|
| Command | Behavior |
|
||||||
|
|---------|----------|
|
||||||
|
| `pos communication telegram --send "text"` | POSTs `sendMessage` to the Bot API (20s timeout); prints `[+] Message sent to chat <id>` or fails with a nonzero exit |
|
||||||
|
| `pos communication telegram --send "text" --token <t> --chat-id <id>` | One-shot override of token/chat ID |
|
||||||
|
| `pos communication telegram test` | Sends a canned test message using the current config |
|
||||||
|
| `pos communication telegram config` | Shows current config (bot token masked) |
|
||||||
|
| `pos communication telegram config set TELEGRAM_BOT_TOKEN=...` | Saves a bot token (600 perms) |
|
||||||
|
| `pos communication telegram config set TELEGRAM_CHAT_ID=...` | Saves the target chat ID |
|
||||||
|
|
||||||
|
The bot token is a secret — it is stored only in `~/.config/linux_post_install/telegram.env` and never in the repo. Requires network access to `api.telegram.org`.
|
||||||
|
|
||||||
|
### flags
|
||||||
|
|
||||||
|
Feature-flag management CLIs (see [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libflagssh--feature-flags)):
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `flag-reader` | List all flags + status (`set: <name>` / `unset: <name>`) |
|
||||||
|
| `flag-reader <name>` | Check one flag; exit 0 if set, 1 if not |
|
||||||
|
| `flag-reader --raw <name>` | Print only the stored value (script-friendly) |
|
||||||
|
| `flag-set <name> [value]` | Set a flag, optionally with a value (requires sudo) |
|
||||||
|
| `flag-clear <name>` | Unset a flag (requires sudo) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Legacy wrappers
|
||||||
|
|
||||||
|
Thin 2-line scripts that `exec pos … "$@"`. All of them still work:
|
||||||
|
|
||||||
|
| Wrapper | Forwards to |
|
||||||
|
|---------|-------------|
|
||||||
|
| `wr-ip` | `pos network ip` |
|
||||||
|
| `wr-checkport` | `pos network checkport` |
|
||||||
|
| `wr-scan-ping` | `pos network scan` |
|
||||||
|
| `wr-docker` | `pos docker` |
|
||||||
|
| `wr-compose` | `pos docker compose` |
|
||||||
|
| `wr-ufw` | `pos system firewall` |
|
||||||
|
| `mp3` | `pos media mp3` |
|
||||||
|
| `mp4` | `pos media mp4` |
|
||||||
|
| `vbox` | `pos docker vbox` |
|
||||||
|
| `ssh-load-all` | `pos ssh load-keys` |
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
Everything in this folder is reference material for the `Linux_post_install` project. The root [README](../README.md) is the short intro + quick start; this folder holds the detail.
|
||||||
|
|
||||||
|
| Document | What it covers |
|
||||||
|
|----------|----------------|
|
||||||
|
| [SCRIPTS.md](SCRIPTS.md) | Core installer scripts: `install.sh`, `preinstall.sh`, `postinstall.sh`, `lib/common.sh`, `lib/flags.sh`, `features/autostart.sh` — purpose, how each works, configuration |
|
||||||
|
| [POS.md](POS.md) | The `pos` CLI: dispatcher, every `pos-*` command, Docker Compose / ScaleTail config, legacy wrappers, flag CLIs |
|
||||||
|
| [APPS.md](APPS.md) | Optional apps: `apps/install.sh` picker, installer conventions, full app catalog |
|
||||||
|
| [SYSTEMD.md](SYSTEMD.md) | Systemd units (`autostart.service`, `ssh-agent.service`), feature-flag gating, bash completion |
|
||||||
|
| [DEV.md](DEV.md) | Developer guide: architecture, conventions, how to add tools/apps/features, commit guidelines |
|
||||||
|
| [AGENT_Context_Project.md](AGENT_Context_Project.md) | Single-source context doc for AI agents working on the repo |
|
||||||
|
| [algorithm.md](algorithm.md) | ASCII diagrams: install flow, `pos` dispatch, compose `up`, config cascade, logging, vbox lifecycle |
|
||||||
|
|
||||||
|
## Quick navigation
|
||||||
|
|
||||||
|
- Just installed and want to use it? → [POS.md](POS.md)
|
||||||
|
- Adding a package? → [SCRIPTS.md → preinstall.sh](SCRIPTS.md#preinstallsh--system-packages)
|
||||||
|
- Adding a CLI tool or app? → [DEV.md](DEV.md)
|
||||||
|
- First deploy of a self-hosted service? → [POS.md → Docker Compose](POS.md#docker-compose--scaletail)
|
||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
# Core Scripts Reference
|
||||||
|
|
||||||
|
Everything that runs during the bootstrap install: `install.sh`, `preinstall.sh`, `postinstall.sh`, the shared libraries, and `features/`. For the `pos` CLI tools see [POS.md](POS.md), for apps see [APPS.md](APPS.md), for services see [SYSTEMD.md](SYSTEMD.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
- [install.sh — the orchestrator](#installsh--the-orchestrator)
|
||||||
|
- [preinstall.sh — system packages](#preinstallsh--system-packages)
|
||||||
|
- [postinstall.sh — user configuration](#postinstallsh--user-configuration)
|
||||||
|
- [lib/common.sh — shared library](#libcommonsh--shared-library)
|
||||||
|
- [lib/flags.sh — feature flags](#libflagssh--feature-flags)
|
||||||
|
- [features/autostart.sh — boot-time feature](#featuresautostartsh--boot-time-feature)
|
||||||
|
- [x64_bin/ — precompiled binaries](#x64_bin--precompiled-binaries)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## install.sh — the orchestrator
|
||||||
|
|
||||||
|
**File:** `install.sh` (run as `./install.sh`)
|
||||||
|
**Purpose:** the entry point. Coordinates all four install phases and the optional apps/features installs.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. **Pre-parse `--no-color`** before anything else, so colors are disabled early (`TERM=dumb` is exported).
|
||||||
|
2. Source `lib/common.sh` (logging, `run`, `spawn`, …) and `lib/flags.sh` (feature flags).
|
||||||
|
3. Parse CLI options.
|
||||||
|
4. For each phase, `should_run <num> <name>` decides whether to run it:
|
||||||
|
- `--skip <phase>` removes a phase (takes precedence).
|
||||||
|
- `--steps <spec>` restricts the run to the listed phases only (`1,3,4` or `1-3`).
|
||||||
|
- Phase map: `1=preinstall`, `2=scripts`, `3=postinstall`, `4=scalepoint` (+ `apps` handled separately).
|
||||||
|
|
||||||
|
The phases:
|
||||||
|
|
||||||
|
| # | Phase | Script/action |
|
||||||
|
|---|-------|----------------|
|
||||||
|
| 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp |
|
||||||
|
| 2 | scripts | Copies `bin/*` → `/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` → `/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
|
||||||
|
| 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd |
|
||||||
|
| 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` |
|
||||||
|
| 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) |
|
||||||
|
|
||||||
|
**Precompiled arch binaries (Phase 2):** `install.sh` picks the source folder from the machine architecture — `x86_64` → `x64_bin/`, `aarch64`/`arm64` → `arm64_bin/` (added later) — and copies every file in it to `/usr/local/bin/` (755). These are manually-compiled tools not available as internet builds (currently `create_ap`, `wihotspot`, `wihotspot-gui`). Dropping an `arm64_bin/` folder later needs no code change.
|
||||||
|
|
||||||
|
**Feature block (Phase 2, only with `--feature`):** for every file in `features/` it copies it to `/usr/local/bin/<name>`. If the destination already exists it asks **"Overwrite existing …? [y/N]"** (default keeps your file), then always sets the feature flag via `flag_set` (name derived as `<filename without .sh>`).
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
No config file — everything is command-line:
|
||||||
|
|
||||||
|
| Option | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| `--apps` | Run the interactive app picker after core install |
|
||||||
|
| `--full` | Core install + every app (non-interactive) |
|
||||||
|
| `--feature` | Install `features/` scripts to `/usr/local/bin/` (prompts on overwrite), sets their flags |
|
||||||
|
| `--dry-run` | Log every action instead of executing. **Note:** applies to `install.sh` itself; `postinstall.sh` runs as a subprocess and does not inherit `DRY_RUN` |
|
||||||
|
| `--skip <phase>` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |
|
||||||
|
| `--steps <spec>` | Run only listed phases: `1,3,4` or `1-3` |
|
||||||
|
| `--no-color` | Disable colored output |
|
||||||
|
| `-h`, `--help` | Show usage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## preinstall.sh — system packages
|
||||||
|
|
||||||
|
**File:** `preinstall.sh`
|
||||||
|
**Purpose:** Phase 1 — installs the base system packages and yt-dlp.
|
||||||
|
**Run:** automatically by `install.sh`, or standalone with `--dry-run`.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. `apt update`.
|
||||||
|
2. Installs the package list.
|
||||||
|
3. Downloads the latest `yt-dlp` binary to `/usr/local/bin/yt-dlp` and makes it executable.
|
||||||
|
4. Verifies a couple of tools (`git --version`, `yt-dlp --version`).
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The package list is the `PACKAGES` array:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PACKAGES=(
|
||||||
|
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
|
||||||
|
hostapd dnsmasq iptables iw
|
||||||
|
ca-certificates gnupg lsb-release
|
||||||
|
python3 python3-pip rclone
|
||||||
|
libqrencode4 libgtk-3-0
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add or remove package names here. `nmap` and `fail2ban` are used later by `pos network scan` and `postinstall.sh`; `hostapd`, `dnsmasq`, `iptables`, `iw` and the GTK/Qr libs support the precompiled hotspot tools (see [x64_bin/ — precompiled binaries](#x64_bin--precompiled-binaries)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## postinstall.sh — user configuration
|
||||||
|
|
||||||
|
**File:** `postinstall.sh` (runs as your user)
|
||||||
|
**Purpose:** Phase 3 — configures the user environment, SSH keys, and systemd services.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
1. **rclone config** — if `config/rclone.conf` exists (gitignored), installs it to `~/.config/rclone/rclone.conf` (600).
|
||||||
|
2. **PATH** — appends a `PATH` line to `~/.bashrc` if not already present.
|
||||||
|
3. **pos bash completion** — installs `completions/pos.bash` to `/usr/local/share/bash-completion/completions/` and sources it from `~/.bashrc`.
|
||||||
|
4. **SSH authorized keys** — if `config/authorized_keys` exists, appends missing keys to `~/.ssh/authorized_keys` (skips comments and duplicates, chmod 600).
|
||||||
|
5. **systemd services** — copies `systemd/*.service` to `/etc/systemd/system/`, daemon-reloads, then enables each service. **`autostart.service` is only enabled when the `autostart` feature flag is set** (see [lib/flags.sh](#libflagssh--feature-flags)); otherwise it's skipped with a hint to run `./install.sh --feature`.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- SSH keys: `config/authorized_keys` (one per line, gitignored).
|
||||||
|
- rclone config: `config/rclone.conf` (gitignored).
|
||||||
|
- The PATH line and completion line are embedded strings at the top of the file — edit there to change them.
|
||||||
|
- The `autostart` flag (set by `./install.sh --feature`) controls whether `autostart.service` gets enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## lib/common.sh — shared library
|
||||||
|
|
||||||
|
**File:** `lib/common.sh` (installed to `/usr/local/bin/common.sh`)
|
||||||
|
**Purpose:** colors, logging, timers, spinners, dry-run-aware execution, and prompts. Sourced by most scripts.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
Auto-disables colors when stdout is not a TTY. The `run` helper is the dry-run hook: scripts that want `--dry-run` support run every side-effecting command through `run`.
|
||||||
|
|
||||||
|
### Configuration / API
|
||||||
|
|
||||||
|
| Function | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `log "msg"` | Green `[+]` status line |
|
||||||
|
| `warn "msg"` | Yellow `[!]` warning |
|
||||||
|
| `err "msg"` | Red `ERROR:` line to stderr, then `exit 1` |
|
||||||
|
| `ok "msg"` | Green `OK` prefix line |
|
||||||
|
| `section "title"` | Cyan-bordered section header |
|
||||||
|
| `step N T "msg"` | Numbered step header (`[N/T] msg`) |
|
||||||
|
| `run cmd…` | Executes the command, or logs `(dry-run)` when `DRY_RUN=1` |
|
||||||
|
| `spawn "msg" cmd…` | Runs with an animated spinner + elapsed time; prints captured stderr and exits on failure |
|
||||||
|
| `timer_start` / `timer_stop` | Track and print elapsed time |
|
||||||
|
| `confirm "prompt" [default]` | Yes/no prompt; default `y` (`[Y/n]`) unless `n` given (`[y/N]`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## lib/flags.sh — feature flags
|
||||||
|
|
||||||
|
**File:** `lib/flags.sh` (installed to `/usr/local/bin/flags.sh`)
|
||||||
|
**Purpose:** a system-wide, per-feature flag store. Flags mark features as installed/opted-in and gate behavior (e.g. systemd enablement) elsewhere.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
One file per flag in `$FLAGS_DIR`. **Presence = set, file content = optional value.** Reads are plain file ops; writes go through `run` + `sudo` so they respect `--dry-run`. Installed by `./install.sh --feature`; also usable directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source lib/flags.sh
|
||||||
|
flag_set autostart # bare flag
|
||||||
|
flag_set app "2.1" # flag with a value
|
||||||
|
flag_is_set autostart # 0 if set, 1 if not
|
||||||
|
flag_value app # prints "2.1"
|
||||||
|
flag_list # names of all set flags
|
||||||
|
flag_clear autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Setting | Location |
|
||||||
|
|---------|----------|
|
||||||
|
| `FLAGS_DIR` (env) | Default `/usr/local/share/linux_post_install/flags` (dir 755, files 644). Overridable via environment for testing |
|
||||||
|
| CLI wrappers | `flag-reader`, `flag-set`, `flag-clear` (see [POS.md](POS.md)) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## features/autostart.sh — boot-time feature
|
||||||
|
|
||||||
|
**File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`)
|
||||||
|
**Purpose:** runs once at boot via `autostart.service` (only when the `autostart` flag is green) and logs basic connectivity status.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
Appends timestamped lines to `~/.autostart.log`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[<date>] autostart running
|
||||||
|
[<date>] Network: online # ping 8.8.8.8 succeeded
|
||||||
|
[<date>] autostart complete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- Log file: `$HOME/.autostart.log` (edit the `LOG` variable at the top).
|
||||||
|
- The script is the one you're *most* likely to customize — this is exactly why it lives in `features/` instead of `bin/`: a plain reinstall never overwrites your edits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## x64_bin/ — precompiled binaries
|
||||||
|
|
||||||
|
**Folder:** `x64_bin/` (future: `arm64_bin/`)
|
||||||
|
**Purpose:** manually-compiled tools that are **not available as prebuilt binaries on the internet**. `install.sh` copies them verbatim into `/usr/local/bin/` on the matching architecture (see [install.sh — the orchestrator](#installsh--the-orchestrator)).
|
||||||
|
|
||||||
|
### Contents
|
||||||
|
|
||||||
|
| File | Type | Purpose | Runtime deps |
|
||||||
|
|------|------|---------|--------------|
|
||||||
|
| `create_ap` | bash script | Create a Wi-Fi access point from the CLI (NAT/Internet sharing) | `hostapd`, `dnsmasq`, `iptables`, `iw` (in `preinstall.sh`) |
|
||||||
|
| `wihotspot` | POSIX wrapper | Launches `wihotspot-gui` (path points at `/usr/local/bin/`) | — |
|
||||||
|
| `wihotspot-gui` | ELF x86-64 | GTK3 GUI for the hotspot (QR code via libqrencode) | `libgtk-3-0`, `libqrencode4` |
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- Managed through the `pos network hotspot` command (see [POS.md → network](POS.md#network)): `hotspot` launches the GUI; `start`/`stop`/`status` wrap `create_ap`.
|
||||||
|
- Add a future `arm64_bin/` folder with the same filenames and it is installed automatically on `aarch64` machines — no `install.sh` change needed.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Systemd & Shell Integration Reference
|
||||||
|
|
||||||
|
The units installed and enabled by `postinstall.sh`, plus the `pos` bash completion.
|
||||||
|
|
||||||
|
- [Services](#services)
|
||||||
|
- [`autostart.service`](#autostartservice)
|
||||||
|
- [`ssh-agent.service`](#ssh-agentservice)
|
||||||
|
- [Feature-flag gating](#feature-flag-gating)
|
||||||
|
- [Bash completion](#bash-completion)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
`postinstall.sh` copies every `systemd/*.service` to `/etc/systemd/system/`, runs `systemctl daemon-reload`, then enables each one (see the gating rule below).
|
||||||
|
|
||||||
|
### autostart.service
|
||||||
|
|
||||||
|
**Purpose:** run `features/autostart.sh` at boot (after the network is online) and keep retrying if it fails.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=My Linux Autostart Script
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/autostart.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration:** point `ExecStart` at your boot script. Because the target script is a *feature*, this unit is only **enabled** when the `autostart` flag is set — the file is still copied, but a skipped feature leaves the unit present-but-disabled.
|
||||||
|
|
||||||
|
### ssh-agent.service
|
||||||
|
|
||||||
|
**Purpose:** a system-wide SSH agent, one shared socket for all sessions (so `pos ssh load-keys` and everyday ssh work without per-login agents).
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=SSH Authentication Agent
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStartPre=mkdir -p /run/ssh-agent
|
||||||
|
ExecStart=/usr/bin/ssh-agent -D -a /run/ssh-agent/socket
|
||||||
|
ExecStartPost=/bin/sh -c 'chmod 666 /run/ssh-agent/socket'
|
||||||
|
ExecStopPost=/bin/sh -c 'rm -f /run/ssh-agent/socket'
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration:** socket at `/run/ssh-agent/socket` (world-readable/writable). `~/.bashrc` (set by `postinstall.sh`) exports `SSH_AUTH_SOCK` to it. Not gated on any feature flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature-flag gating
|
||||||
|
|
||||||
|
The systemd loop in `postinstall.sh` special-cases `autostart.service`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ "$svc_name" = "autostart.service" ] && ! flag_is_set autostart; then
|
||||||
|
warn "autostart feature not installed — skipping autostart.service (run ./install.sh --feature)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the flag with `./install.sh --feature` (or `flag-set autostart`). See [SCRIPTS.md → lib/flags.sh](SCRIPTS.md#libflagssh--feature-flags).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bash completion
|
||||||
|
|
||||||
|
**File:** `completions/pos.bash`
|
||||||
|
**Purpose:** tab-completion for the `pos` CLI.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
- **Dynamically discovers** subcommands by listing executable `pos-*` files next to the `pos` binary — no hard-coded command list, so new tools complete automatically.
|
||||||
|
- Works with the `bash-completion` package (`_init_completion`) and falls back to a manual init if it isn't loaded.
|
||||||
|
- Provides completion for the first two words of `pos <category> <command>`.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Installed by `postinstall.sh` to `/usr/local/share/bash-completion/completions/pos.bash` and sourced from `~/.bashrc`. To load it manually: `source completions/pos.bash` (or copy into `/etc/bash_completion.d/`).
|
||||||
@@ -325,7 +325,7 @@ pos docker compose up jellyfin
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. `pos-vbox` Lifecycle
|
## 6. `pos docker vbox` Lifecycle
|
||||||
|
|
||||||
```
|
```
|
||||||
create lab1
|
create lab1
|
||||||
@@ -2,22 +2,21 @@
|
|||||||
|
|
||||||
> One command turns a bare Debian/Ubuntu install into a fully productive machine.
|
> One command turns a bare Debian/Ubuntu install into a fully productive machine.
|
||||||
|
|
||||||
---
|
## What is this
|
||||||
|
|
||||||
## What Is This
|
After reinstalling Linux you usually need to install packages, set up SSH, configure a firewall, and install apps. This repo automates all of that in one go.
|
||||||
|
|
||||||
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.
|
It is a **personal toolkit** — a bootstrap script, a unified `pos` CLI for everyday tasks, optional app installers, and self-hosted services via ScaleTail + Tailscale.
|
||||||
|
|
||||||
**What you get:**
|
**What you get:**
|
||||||
|
|
||||||
- **25+ system packages** installed automatically (git, curl, tmux, ufw, fail2ban, etc.)
|
- 25+ system packages installed automatically
|
||||||
- **Unified `pos` CLI** — one command for network, Docker, media, system, and SSH tasks
|
- The `pos` CLI: network, Docker (compose + vbox VMs), media, system, SSH, USB, and communication (Telegram) tools
|
||||||
- **119+ self-hosted services** via ScaleTail + Tailscale (Jellyfin, Home Assistant, etc.)
|
- Wi-Fi hotspot tools (`create_ap`, `wihotspot-gui`) via `pos network hotspot`
|
||||||
- **15 optional desktop apps** (VS Code, Docker Desktop, Brave, OBS, etc.) — pick what you want
|
- 15 optional desktop apps (VS Code, Brave, OBS, Tailscale, …) — pick what you want
|
||||||
- **systemd services** for SSH agent and boot-time automation
|
- 119+ self-hosted services with Tailscale access (Jellyfin, Home Assistant, …)
|
||||||
- **Everything in `/usr/local/bin/`** — you can delete the repo after install
|
- systemd services for SSH agent and boot-time automation
|
||||||
|
- Everything lands in `/usr/local/bin/` — you can delete the repo after install
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -25,168 +24,28 @@ After reinstalling Linux, you usually need to install packages, set up SSH, conf
|
|||||||
git clone https://gitea.skink-platy.ts.net/admin/Linux_post_install.git
|
git clone https://gitea.skink-platy.ts.net/admin/Linux_post_install.git
|
||||||
cd Linux_post_install
|
cd Linux_post_install
|
||||||
./install.sh # core: packages + CLI + services + ScaleTail
|
./install.sh # core: packages + CLI + services + ScaleTail
|
||||||
./install.sh --apps # core + interactive app picker
|
./install.sh --feature # also install features/ scripts (asks before overwriting)
|
||||||
./install.sh --full # core + all apps (non-interactive)
|
./install.sh --apps # also install optional desktop apps (interactive)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Flags:**
|
|
||||||
|
|
||||||
| Flag | What it does |
|
| Flag | What it does |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `--apps` | Run interactive app picker after core install |
|
| `--feature` | Install `features/` scripts to `/usr/local/bin/`, sets their flags |
|
||||||
| `--full` | Core install + all apps (no prompts) |
|
| `--apps` | Interactive app picker after core install |
|
||||||
| `--dry-run` | Preview without executing anything |
|
| `--full` | Core install + all apps (non-interactive) |
|
||||||
| `--skip <phase>` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint` |
|
| `--dry-run` | Preview without executing |
|
||||||
| `--steps <spec>` | Run specific phases only, e.g. `--steps 1,3` or `--steps 1-3` |
|
| `--skip <phase>` | Skip a phase: `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |
|
||||||
|
| `--steps <spec>` | Run only specific phases, e.g. `--steps 1,3` |
|
||||||
| `--no-color` | Disable colored output |
|
| `--no-color` | Disable colored output |
|
||||||
|
|
||||||
---
|
## Documentation
|
||||||
|
|
||||||
## What Gets Installed
|
| Topic | Where |
|
||||||
|
|-------|-------|
|
||||||
| Phase | Script | What happens |
|
| Docs index | [DOC/README.md](DOC/README.md) |
|
||||||
|-------|--------|-------------|
|
| Core scripts (installer, libs, features) — how they work + config | [DOC/SCRIPTS.md](DOC/SCRIPTS.md) |
|
||||||
| 1 | `preinstall.sh` | `apt update` + 25+ packages + yt-dlp + fail2ban |
|
| `pos` CLI reference (all commands, compose config, wrappers) | [DOC/POS.md](DOC/POS.md) |
|
||||||
| 2 | `install.sh` | Copies all `bin/` tools to `/usr/local/bin/` |
|
| Optional apps (picker + full catalog) | [DOC/APPS.md](DOC/APPS.md) |
|
||||||
| 3 | `postinstall.sh` | Configures fail2ban, SSH agent, PATH, bash completion, systemd services |
|
| Systemd services & bash completion | [DOC/SYSTEMD.md](DOC/SYSTEMD.md) |
|
||||||
| 4 | ScaleTail clone | Downloads 119+ Docker Compose templates with Tailscale sidecar |
|
| Developer guide (add tools/apps/features) | [DOC/DEV.md](DOC/DEV.md) |
|
||||||
| 5 (opt) | `apps/install.sh` | Installs desktop apps you select |
|
| Algorithm diagrams | [DOC/algorithm.md](DOC/algorithm.md) |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The `pos` CLI
|
|
||||||
|
|
||||||
After install, use the `pos` command for everything:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos # list available categories
|
|
||||||
pos help network # help for a specific category
|
|
||||||
```
|
|
||||||
|
|
||||||
### Network
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos network ip # Show interfaces, routes, public IP
|
|
||||||
pos network checkport 192.168.1.1:80 # Check if a TCP port is open
|
|
||||||
pos network scan 192.168.8.0/24 # Fast parallel ping sweep
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos docker ps # List containers with health, IPs, ports
|
|
||||||
pos docker health # Health dashboard (exits 1 if unhealthy)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Compose (ScaleTail — 119+ self-hosted services)
|
|
||||||
|
|
||||||
Each service runs with a Tailscale sidecar and gets its own `tail-xxxxx.ts.net` URL.
|
|
||||||
|
|
||||||
**Quick start:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Set your Tailscale auth key (required once)
|
|
||||||
pos docker compose config set TS_AUTHKEY=tskey-auth-xxxxx
|
|
||||||
|
|
||||||
# 2. Deploy a service
|
|
||||||
pos docker compose up jellyfin
|
|
||||||
|
|
||||||
# 3. Open https://jellyfin.tail-xxxxx.ts.net
|
|
||||||
```
|
|
||||||
|
|
||||||
**All commands:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos docker compose ls # List available service templates
|
|
||||||
pos docker compose installed # List deployed services
|
|
||||||
pos docker compose up jellyfin # Deploy or start a service
|
|
||||||
pos docker compose down actual-budget # Stop a service
|
|
||||||
pos docker compose logs home-assistant -f # Tail logs
|
|
||||||
pos docker compose restart home-assistant # Restart a service
|
|
||||||
pos docker compose update # Pull latest templates + refresh deployed compose files
|
|
||||||
pos docker compose config # Show current configuration
|
|
||||||
pos docker compose config set TZ=Asia/Tokyo # Set a global default
|
|
||||||
pos docker compose config edit # Open config in editor
|
|
||||||
```
|
|
||||||
|
|
||||||
**Config strategy — three layers:**
|
|
||||||
|
|
||||||
| Layer | File | Purpose |
|
|
||||||
|-------|------|---------|
|
|
||||||
| Template defaults | `/usr/local/share/linux_post_install/scale-tail/services/<name>/.env` | Per-service defaults from ScaleTail |
|
|
||||||
| Global config | `~/.config/linux_post_install/compose.env` | Your defaults — applies to all services |
|
|
||||||
| Per-service | `/srv/<service>/.env` | Actual config — created on first deploy, **never overwritten** |
|
|
||||||
|
|
||||||
Set global defaults once, then every `up` fills them into the new service's `.env`.
|
|
||||||
|
|
||||||
**Paths:**
|
|
||||||
|
|
||||||
- Templates: `/usr/local/share/linux_post_install/scale-tail/services/`
|
|
||||||
- Deployments: `/srv/<service>/` (configurable via `SERVICES_BASE`)
|
|
||||||
- Global config: `~/.config/linux_post_install/compose.env`
|
|
||||||
|
|
||||||
**Config keys:**
|
|
||||||
|
|
||||||
| Key | What it does |
|
|
||||||
|-----|-------------|
|
|
||||||
| `TS_AUTHKEY` | Tailscale auth key (required for sidecar networking) |
|
|
||||||
| `TZ` | Timezone for the service |
|
|
||||||
| `DNS_SERVER` | Custom DNS server |
|
|
||||||
| `SERVICES_BASE` | Where services are deployed (default: `/srv`)
|
|
||||||
|
|
||||||
### VBox (disposable Docker containers)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos vbox create lab1 # Create — prompts to enter
|
|
||||||
pos vbox create lab1 --dir . # Bind mount current directory
|
|
||||||
pos vbox enter lab1 # Auto-starts if stopped
|
|
||||||
pos vbox ls # List vbox containers only
|
|
||||||
pos vbox stop/start/rm lab1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Media
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos media mp3 https://youtube.com/watch?v=... # Audio → MP3
|
|
||||||
pos media mp4 https://youtube.com/watch?v=... # Video with format selection
|
|
||||||
```
|
|
||||||
|
|
||||||
### System
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo pos system firewall # Interactive UFW manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pos ssh load-keys # Load all SSH keys into agent
|
|
||||||
```
|
|
||||||
|
|
||||||
The `ssh-agent.service` runs at boot. `SSH_AUTH_SOCK` is set in `~/.bashrc`.
|
|
||||||
|
|
||||||
### Legacy wrappers
|
|
||||||
|
|
||||||
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`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Optional Apps
|
|
||||||
|
|
||||||
Install with `./apps/install.sh` (interactive), `./apps/install.sh --all`, or by name:
|
|
||||||
|
|
||||||
| Category | Apps |
|
|
||||||
|----------|------|
|
|
||||||
| Browsers | Brave |
|
|
||||||
| Development | opencode, VS Code |
|
|
||||||
| Media | OBS Studio, scrcpy, VLC |
|
|
||||||
| Networking | NetBird, Tailscale, ZeroTier |
|
|
||||||
| Remote Access | Termius, VNC Viewer |
|
|
||||||
| System | Docker Engine, QEMU + KVM |
|
|
||||||
| Utilities | AFFiNE, btop, LocalSend |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
See [DEV.md](DEV.md) for architecture, conventions, and how to add or modify tools.
|
|
||||||
|
|||||||
+11
-1
@@ -15,4 +15,14 @@ install_brave() {
|
|||||||
spawn "Installing brave-browser" sudo apt install -y brave-browser
|
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
|
||||||
|
|||||||
@@ -13,4 +13,13 @@ install_opencode() {
|
|||||||
log "Add to PATH: export PATH=\"\$HOME/.opencode/bin:\$PATH\""
|
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
|
||||||
|
|||||||
@@ -15,4 +15,14 @@ install_vscode() {
|
|||||||
spawn "Installing code" sudo apt install -y code
|
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
|
||||||
|
|||||||
+47
-19
@@ -6,26 +6,32 @@ usage() {
|
|||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: bash apps/install.sh [OPTIONS] [app ...]
|
Usage: bash apps/install.sh [OPTIONS] [app ...]
|
||||||
|
|
||||||
Install optional desktop applications.
|
Install or uninstall optional desktop applications.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--all Install all available apps without prompting
|
--all Apply action to all available apps without prompting
|
||||||
-h, --help Show this help message
|
--uninstall Uninstall the selected apps instead of installing
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bash apps/install.sh # interactive selection
|
bash apps/install.sh # interactive install selection
|
||||||
bash apps/install.sh --all # install everything
|
bash apps/install.sh --all # install everything
|
||||||
bash apps/install.sh brave vscode # install specific apps
|
bash apps/install.sh brave vscode # install specific apps
|
||||||
|
bash apps/install.sh --uninstall # interactive uninstall selection
|
||||||
|
bash apps/install.sh --uninstall --all # uninstall everything
|
||||||
|
bash apps/install.sh --uninstall brave # uninstall specific apps
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ALL=0
|
ALL=0
|
||||||
|
UNINSTALL=0
|
||||||
POSITIONAL=()
|
POSITIONAL=()
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--all) ALL=1; shift ;;
|
--all) ALL=1; shift ;;
|
||||||
|
--uninstall) UNINSTALL=1; shift ;;
|
||||||
-h|--help) usage ;;
|
-h|--help) usage ;;
|
||||||
-*) err "Unknown option: $1" ;;
|
-*) err "Unknown option: $1" ;;
|
||||||
*) POSITIONAL+=("$1"); shift ;;
|
*) POSITIONAL+=("$1"); shift ;;
|
||||||
@@ -62,11 +68,11 @@ for cat_dir in "$APPS_DIR"/*/; do
|
|||||||
[ "$has_apps" -eq 0 ] && continue
|
[ "$has_apps" -eq 0 ] && continue
|
||||||
|
|
||||||
CATEGORIES+=("$cat_name")
|
CATEGORIES+=("$cat_name")
|
||||||
CAT_APPS["$cat_name"]=()
|
CAT_APPS["$cat_name"]=""
|
||||||
for f in "$cat_dir"*.sh; do
|
for f in "$cat_dir"*.sh; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
app_name=$(basename "$f" .sh)
|
app_name=$(basename "$f" .sh)
|
||||||
CAT_APPS["$cat_name"]+=("$app_name")
|
CAT_APPS["$cat_name"]+=" $app_name"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -74,7 +80,7 @@ done
|
|||||||
find_app_category() {
|
find_app_category() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
for cat in "${CATEGORIES[@]}"; do
|
for cat in "${CATEGORIES[@]}"; do
|
||||||
for a in "${CAT_APPS[$cat]}"; do
|
for a in ${CAT_APPS[$cat]}; do
|
||||||
[ "$a" == "$app" ] && { echo "$cat"; return 0; }
|
[ "$a" == "$app" ] && { echo "$cat"; return 0; }
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -96,7 +102,7 @@ if [ "${#POSITIONAL[@]}" -gt 0 ]; then
|
|||||||
# ── Mode 2: --all ─────────────────────────────────────────────
|
# ── Mode 2: --all ─────────────────────────────────────────────
|
||||||
elif [ "$ALL" -eq 1 ]; then
|
elif [ "$ALL" -eq 1 ]; then
|
||||||
for cat in "${CATEGORIES[@]}"; do
|
for cat in "${CATEGORIES[@]}"; do
|
||||||
for app in "${CAT_APPS[$cat]}"; do
|
for app in ${CAT_APPS[$cat]}; do
|
||||||
SELECTED+=("$app")
|
SELECTED+=("$app")
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -104,14 +110,22 @@ elif [ "$ALL" -eq 1 ]; then
|
|||||||
# ── Mode 3: interactive TUI ───────────────────────────────────
|
# ── Mode 3: interactive TUI ───────────────────────────────────
|
||||||
else
|
else
|
||||||
section "Optional Applications"
|
section "Optional Applications"
|
||||||
echo "Select apps to install (y/n for each):"
|
if [ "$UNINSTALL" -eq 1 ]; then
|
||||||
|
echo "Select apps to uninstall (y/n for each):"
|
||||||
|
else
|
||||||
|
echo "Select apps to install (y/n for each):"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
for cat in "${CATEGORIES[@]}"; do
|
for cat in "${CATEGORIES[@]}"; do
|
||||||
display="${CAT_NAMES[$cat]:-$cat}"
|
display="${CAT_NAMES[$cat]:-$cat}"
|
||||||
echo " $display"
|
echo " $display"
|
||||||
for app in "${CAT_APPS[$cat]}"; do
|
for app in ${CAT_APPS[$cat]}; do
|
||||||
read -rp " Install ${app}? [y/N]: " yn
|
if [ "$UNINSTALL" -eq 1 ]; then
|
||||||
|
read -rp " Remove ${app}? [y/N]: " yn
|
||||||
|
else
|
||||||
|
read -rp " Install ${app}? [y/N]: " yn
|
||||||
|
fi
|
||||||
if [[ "$yn" =~ ^[Yy] ]]; then
|
if [[ "$yn" =~ ^[Yy] ]]; then
|
||||||
SELECTED+=("$app")
|
SELECTED+=("$app")
|
||||||
fi
|
fi
|
||||||
@@ -120,11 +134,15 @@ else
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Install selected apps ──────────────────────────────────────
|
# ── Apply action to selected apps ─────────────────────────────
|
||||||
[ "${#SELECTED[@]}" -eq 0 ] && { warn "No apps selected"; exit 0; }
|
[ "${#SELECTED[@]}" -eq 0 ] && { warn "No apps selected"; exit 0; }
|
||||||
|
|
||||||
echo
|
echo
|
||||||
section "Installing ${SELECTED[*]}"
|
if [ "$UNINSTALL" -eq 1 ]; then
|
||||||
|
section "Uninstalling ${SELECTED[*]}"
|
||||||
|
else
|
||||||
|
section "Installing ${SELECTED[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
timer_start
|
timer_start
|
||||||
count=1
|
count=1
|
||||||
@@ -132,12 +150,22 @@ total=${#SELECTED[@]}
|
|||||||
for app in "${SELECTED[@]}"; do
|
for app in "${SELECTED[@]}"; do
|
||||||
cat=$(find_app_category "$app")
|
cat=$(find_app_category "$app")
|
||||||
step "$count" "$total" "$app"
|
step "$count" "$total" "$app"
|
||||||
bash "$APPS_DIR/$cat/$app.sh"
|
if [ "$UNINSTALL" -eq 1 ]; then
|
||||||
|
bash "$APPS_DIR/$cat/$app.sh" uninstall
|
||||||
|
else
|
||||||
|
bash "$APPS_DIR/$cat/$app.sh"
|
||||||
|
fi
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
if [ "$UNINSTALL" -eq 1 ]; then
|
||||||
echo "${GREEN} Apps installed ($(timer_stop))${RESET}"
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
echo "${GREEN} Apps uninstalled ($(timer_stop))${RESET}"
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
|
else
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
|
echo "${GREEN} Apps installed ($(timer_stop))${RESET}"
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
|
fi
|
||||||
|
|||||||
+10
-1
@@ -7,4 +7,13 @@ install_obs() {
|
|||||||
spawn "Installing obs-studio" sudo apt install -y obs-studio
|
spawn "Installing obs-studio" sudo apt install -y obs-studio
|
||||||
}
|
}
|
||||||
|
|
||||||
install_obs
|
uninstall_obs() {
|
||||||
|
command -v obs &>/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
|
||||||
|
|||||||
+10
-1
@@ -50,4 +50,13 @@ EOF
|
|||||||
log "scrcpy $version installed (adb included in the bundle)"
|
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
|
||||||
|
|||||||
+10
-1
@@ -7,4 +7,13 @@ install_vlc() {
|
|||||||
spawn "Installing vlc" sudo apt install -y 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
|
||||||
|
|||||||
@@ -11,4 +11,14 @@ install_netbird() {
|
|||||||
log "Join a network: sudo netbird up --setup-key <key>"
|
log "Join a network: sudo netbird up --setup-key <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
|
||||||
|
|||||||
@@ -11,4 +11,14 @@ install_tailscale() {
|
|||||||
log "Start tailscale: sudo tailscale up"
|
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
|
||||||
|
|||||||
@@ -11,4 +11,14 @@ install_zerotier() {
|
|||||||
log "Join a network: sudo zerotier-cli join <network-id>"
|
log "Join a network: sudo zerotier-cli join <network-id>"
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -17,4 +17,13 @@ install_termius() {
|
|||||||
log "Termius installed — launch with '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
|
||||||
|
|||||||
@@ -7,4 +7,13 @@ install_vnc_viewer() {
|
|||||||
spawn "Installing tigervnc-viewer" sudo apt install -y tigervnc-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
|
||||||
|
|||||||
+20
-1
@@ -12,4 +12,23 @@ install_docker() {
|
|||||||
warn "Log out and back in for docker group to take effect"
|
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
|
||||||
|
|||||||
+13
-1
@@ -15,4 +15,16 @@ install_qemu() {
|
|||||||
warn "Log out and back in for libvirt/kvm groups to take effect"
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+10
-1
@@ -7,4 +7,13 @@ install_btop() {
|
|||||||
spawn "Installing btop" sudo apt install -y 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
|
||||||
|
|||||||
@@ -16,4 +16,15 @@ install_localsend() {
|
|||||||
spawn "Installing localsend" sudo flatpak install -y flathub org.localsend.localsend_app
|
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
|
||||||
|
|||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_tsui() {
|
||||||
|
command -v tsui &>/dev/null && { log "tsui already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing tsui" bash -c "
|
||||||
|
curl -fsSL https://neuralink.com/tsui/install.sh | bash
|
||||||
|
"
|
||||||
|
log "Run tsui: sudo tsui"
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall_tsui() {
|
||||||
|
command -v tsui &>/dev/null || { log "tsui not installed"; return 0; }
|
||||||
|
spawn "Removing tsui" sudo rm -f /usr/local/bin/tsui
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
uninstall) uninstall_tsui ;;
|
||||||
|
*) install_tsui ;;
|
||||||
|
esac
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: flag-clear <name>
|
||||||
|
|
||||||
|
Unset a feature flag (remove it from $FLAGS_DIR).
|
||||||
|
Requires sudo.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
flag_clear "$1"
|
||||||
|
ok "flag cleared: $1"
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: flag-reader [OPTIONS] [name ...]
|
||||||
|
|
||||||
|
Show feature flag state. Flags live in $FLAGS_DIR
|
||||||
|
|
||||||
|
flag-reader list all flags with status
|
||||||
|
flag-reader <name>... status per flag (exit 0 if all are set)
|
||||||
|
flag-reader --raw <name> print the raw stored value only (script-friendly)
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RAW=0
|
||||||
|
NAMES=()
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
--raw) RAW=1 ;;
|
||||||
|
-*) echo "ERROR: unknown option: $arg" >&2; exit 1 ;;
|
||||||
|
*) NAMES+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$RAW" -eq 1 ]; then
|
||||||
|
[ "${#NAMES[@]}" -eq 1 ] || { echo "ERROR: --raw requires exactly one name" >&2; exit 1; }
|
||||||
|
flag_value "${NAMES[0]}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#NAMES[@]}" -eq 0 ]; then
|
||||||
|
if [ -d "$FLAGS_DIR" ] && [ -n "$(ls -A "$FLAGS_DIR" 2>/dev/null)" ]; then
|
||||||
|
for f in "$FLAGS_DIR"/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
flag_status "$(basename "$f")"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
warn "no flags set"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
for name in "${NAMES[@]}"; do
|
||||||
|
if flag_is_set "$name"; then
|
||||||
|
ok "$(flag_status "$name")"
|
||||||
|
else
|
||||||
|
warn "$(flag_status "$name")"
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit "$rc"
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: flag-set <name> [value]
|
||||||
|
|
||||||
|
Set a feature flag (green), optionally storing a value.
|
||||||
|
Writes to $FLAGS_DIR (requires sudo).
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
flag_set "$1" "${2:-}"
|
||||||
|
ok "flag set: $1"
|
||||||
@@ -25,9 +25,60 @@ _pos_commands() {
|
|||||||
echo "${cmds[*]}"
|
echo "${cmds[*]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Category list (sorted, filename-derived) ─────────────────────
|
||||||
|
_pos_category_list() {
|
||||||
|
local -A cats=()
|
||||||
|
local cmd cat f
|
||||||
|
for f in "$self"/pos-*; do
|
||||||
|
[ -x "$f" ] || continue
|
||||||
|
cmd="${f##*/pos-}"
|
||||||
|
cat="${cmd%%-*}"
|
||||||
|
[ "$cat" = "$cmd" ] && continue
|
||||||
|
cats["$cat"]+="${cmd#*-} "
|
||||||
|
done
|
||||||
|
local c line subs joined
|
||||||
|
for c in $(printf '%s\n' "${!cats[@]}" | sort); do
|
||||||
|
subs=(${cats[$c]})
|
||||||
|
joined="$(IFS='|'; echo "${subs[*]}")"
|
||||||
|
printf " %-14s%s\n" "$c" "$joined"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Category helpers ────────────────────────────────────────────
|
||||||
|
_pos_category_exists() {
|
||||||
|
local cat="${1:-}" f
|
||||||
|
[ -n "$cat" ] || return 1
|
||||||
|
for f in "$self"/pos-"$cat"-*; do
|
||||||
|
[ -x "$f" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos_category_help() {
|
||||||
|
local cat="$1" sub subs=()
|
||||||
|
local f
|
||||||
|
for f in "$self"/pos-"$cat"-*; do
|
||||||
|
[ -x "$f" ] || continue
|
||||||
|
subs+=("${f##*/pos-$cat-}")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "pos $cat — $cat tools"
|
||||||
|
echo
|
||||||
|
echo "USAGE"
|
||||||
|
echo " pos $cat <command> [args]"
|
||||||
|
echo
|
||||||
|
echo "COMMANDS"
|
||||||
|
printf " %s\n" "${subs[@]}"
|
||||||
|
echo
|
||||||
|
echo "Run 'pos $cat <command> --help' for details on a command."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# ── Help text ──────────────────────────────────────────────────
|
# ── Help text ──────────────────────────────────────────────────
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
local cats
|
||||||
|
cats="$(_pos_category_list)"
|
||||||
|
cat <<EOF
|
||||||
pos — Personal OS Toolkit
|
pos — Personal OS Toolkit
|
||||||
|
|
||||||
A unified CLI for network, docker, media, system,
|
A unified CLI for network, docker, media, system,
|
||||||
@@ -37,37 +88,41 @@ USAGE
|
|||||||
pos <category> <command> [args]
|
pos <category> <command> [args]
|
||||||
|
|
||||||
CATEGORIES
|
CATEGORIES
|
||||||
network ip | checkport | scan
|
$cats
|
||||||
docker ps | compose
|
|
||||||
media mp3 | mp4
|
|
||||||
system firewall
|
|
||||||
ssh load-keys
|
|
||||||
vbox create | enter | stop | start | rm | ls
|
|
||||||
|
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
pos network ip Show interfaces, routes, public IP
|
pos network ip Show interfaces, routes, public IP
|
||||||
pos network checkport 10.0.0.1:80 Check if a TCP port is open
|
pos network checkport 10.0.0.1:80 Check if a TCP port is open
|
||||||
pos network scan 192.168.1.0/24 Fast parallel ping sweep
|
pos network scan 192.168.1.0/24 Fast parallel ping sweep
|
||||||
|
pos network hotspot Launch wihotspot-gui
|
||||||
|
|
||||||
pos docker ps List containers (health, IPs, ports)
|
pos docker ps List containers (health, IPs, ports)
|
||||||
pos docker compose ls List available ScaleTail services
|
pos docker compose ls List available ScaleTail services
|
||||||
pos docker compose up jellyfin Deploy a service with Tailscale
|
pos docker compose up jellyfin Deploy a service with Tailscale
|
||||||
|
pos docker health One-glance health dashboard
|
||||||
|
|
||||||
pos media mp3 <url> Download audio as MP3
|
pos media mp3 <url> Download audio as MP3
|
||||||
pos media mp4 <url> Download video as MP4
|
pos media mp4 <url> Download video as MP4
|
||||||
|
|
||||||
pos system firewall Interactive UFW manager
|
pos system firewall Interactive UFW manager
|
||||||
|
pos system backup /srv/project Encrypted (AES-256) folder snapshot
|
||||||
|
pos system backup --service Pick a folder from /srv or ~/srv
|
||||||
|
|
||||||
pos ssh load-keys Load all SSH keys into agent
|
pos ssh load-keys Load all SSH keys into agent
|
||||||
|
|
||||||
pos vbox create lab1 Create disposable Docker VM
|
pos usb server --ls List USB devices + connected clients
|
||||||
pos vbox create lab1 --dir . Create VM using current directory
|
|
||||||
pos vbox enter lab1 Shell into a Docker VM
|
pos communication telegram --send "Backup done"
|
||||||
pos vbox ls List Docker VMs
|
Send a Telegram message
|
||||||
|
|
||||||
|
pos docker vbox create lab1 Create disposable Docker VM
|
||||||
|
pos docker vbox create lab1 --dir . Create VM using current directory
|
||||||
|
pos docker vbox enter lab1 Shell into a Docker VM
|
||||||
|
pos docker vbox ls List Docker VMs
|
||||||
|
|
||||||
HELP
|
HELP
|
||||||
pos help <command> Show help for a command
|
pos <category> --help Show a category's commands
|
||||||
pos --help Show this help
|
pos help <command> Show help for a command
|
||||||
|
pos --help Show this help
|
||||||
|
|
||||||
LEGACY WRAPPERS
|
LEGACY WRAPPERS
|
||||||
wr-ip, wr-checkport, wr-scan-ping, wr-docker,
|
wr-ip, wr-checkport, wr-scan-ping, wr-docker,
|
||||||
@@ -86,12 +141,13 @@ esac
|
|||||||
if [ "${1:-}" = "help" ]; then
|
if [ "${1:-}" = "help" ]; then
|
||||||
shift
|
shift
|
||||||
[ $# -eq 0 ] && usage
|
[ $# -eq 0 ] && usage
|
||||||
cmd="pos-${1// /-}"
|
cmd="pos-$*"
|
||||||
|
cmd="${cmd// /-}"
|
||||||
if command -v "$cmd" &>/dev/null; then
|
if command -v "$cmd" &>/dev/null; then
|
||||||
exec "$cmd" --help
|
exec "$cmd" --help
|
||||||
fi
|
fi
|
||||||
[ -x "$self/$cmd" ] && exec "$self/$cmd" --help
|
[ -x "$self/$cmd" ] && exec "$self/$cmd" --help
|
||||||
echo "pos: unknown command '$1'" >&2
|
echo "pos: unknown command '$*'" >&2
|
||||||
echo "Run 'pos --help' to see available commands." >&2
|
echo "Run 'pos --help' to see available commands." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -100,6 +156,13 @@ fi
|
|||||||
args=("$@")
|
args=("$@")
|
||||||
n=${#args[@]}
|
n=${#args[@]}
|
||||||
|
|
||||||
|
# ── Category help: pos <category> [--help] ─────────────────────
|
||||||
|
if _pos_category_exists "${args[0]:-}"; then
|
||||||
|
if [ "$n" -eq 1 ] || { [ "$n" -eq 2 ] && { [ "${args[1]}" = "-h" ] || [ "${args[1]}" = "--help" ]; }; }; then
|
||||||
|
_pos_category_help "${args[0]}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Logging setup ──────────────────────────────────────────────
|
# ── Logging setup ──────────────────────────────────────────────
|
||||||
LOG_DIR="$HOME/.local/share/linux_post_install/logs"
|
LOG_DIR="$HOME/.local/share/linux_post_install/logs"
|
||||||
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
||||||
@@ -109,7 +172,7 @@ MAIN_LOG="$LOG_DIR/pos.log"
|
|||||||
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
log_cmd() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $* → exit $2" >> "$MAIN_LOG"; }
|
||||||
|
|
||||||
# Commands that read from stdin interactively — only log invocation
|
# Commands that read from stdin interactively — only log invocation
|
||||||
INTERACTIVE_CMDS="system-firewall media-mp4"
|
INTERACTIVE_CMDS="system-firewall media-mp4 system-backup usb-server"
|
||||||
|
|
||||||
for ((i=n-1; i>=0; i--)); do
|
for ((i=n-1; i>=0; i--)); do
|
||||||
cmd="pos"
|
cmd="pos"
|
||||||
|
|||||||
Executable
+140
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# POS: communication telegram — Send Telegram messages via Bot API (--send, test, config set)
|
||||||
|
# POS_FLAGS: --send
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/telegram.env"
|
||||||
|
API="https://api.telegram.org"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos communication telegram [command] [args]
|
||||||
|
|
||||||
|
Send Telegram messages via the Bot API.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
--send "text" Send a text message to the configured chat
|
||||||
|
--send "text" --token <t> --chat-id <id> Override token/chat for one send
|
||||||
|
test Send a test message using the current config
|
||||||
|
config Show current config (token masked)
|
||||||
|
config set KEY=VALUE Set TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID
|
||||||
|
|
||||||
|
Config file: $CONFIG_FILE
|
||||||
|
Keys: TELEGRAM_BOT_TOKEN | TELEGRAM_CHAT_ID
|
||||||
|
|
||||||
|
Precedence: CLI flags > environment > config file.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos communication telegram config set TELEGRAM_BOT_TOKEN=123456:ABC-DEF
|
||||||
|
pos communication telegram config set TELEGRAM_CHAT_ID=987654321
|
||||||
|
pos communication telegram --send "Backup finished"
|
||||||
|
pos communication telegram test
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
[ -f "$CONFIG_FILE" ] || return 0
|
||||||
|
local k v
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
[ -n "$k" ] || continue
|
||||||
|
case "$k" in
|
||||||
|
\#*) continue ;;
|
||||||
|
esac
|
||||||
|
v="${v%\"}"; v="${v#\"}"; v="${v%\'}"; v="${v#\'}"
|
||||||
|
if [ -z "${!k:-}" ]; then
|
||||||
|
export "$k"="$v"
|
||||||
|
fi
|
||||||
|
done < <(grep -E '^[A-Z_]+=' "$CONFIG_FILE" || true)
|
||||||
|
}
|
||||||
|
|
||||||
|
mask_token() {
|
||||||
|
local t="$1"
|
||||||
|
[ -z "$t" ] && { echo "(not set)"; return; }
|
||||||
|
if [ "${#t}" -le 12 ]; then
|
||||||
|
echo "${t:0:4}... (${#t} chars)"
|
||||||
|
else
|
||||||
|
echo "${t:0:6}...${t: -4} (${#t} chars)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_send() {
|
||||||
|
local text="$1"
|
||||||
|
[ -z "${TELEGRAM_BOT_TOKEN:-}" ] && err "No bot token — run 'pos communication telegram config set TELEGRAM_BOT_TOKEN=...'"
|
||||||
|
[ -z "${TELEGRAM_CHAT_ID:-}" ] && err "No chat id — run 'pos communication telegram config set TELEGRAM_CHAT_ID=...'"
|
||||||
|
curl -fsS -m 20 -X POST "$API/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
|
||||||
|
--data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
|
||||||
|
--data-urlencode "text=${text}" >/dev/null
|
||||||
|
echo "[+] Message sent to chat ${TELEGRAM_CHAT_ID}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_config() {
|
||||||
|
load_config
|
||||||
|
echo "Config: $CONFIG_FILE"
|
||||||
|
echo " TELEGRAM_BOT_TOKEN = $(mask_token "${TELEGRAM_BOT_TOKEN:-}")"
|
||||||
|
echo " TELEGRAM_CHAT_ID = ${TELEGRAM_CHAT_ID:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_config_set() {
|
||||||
|
local kv="$1"
|
||||||
|
local key="${kv%%=*}"
|
||||||
|
local val="${kv#*=}"
|
||||||
|
case "$key" in
|
||||||
|
TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID) ;;
|
||||||
|
*) err "Unknown key '$key' (allowed: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID)" ;;
|
||||||
|
esac
|
||||||
|
[ -z "$val" ] && err "No value given (expected $key=...)"
|
||||||
|
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
if grep -q "^${key}=" "$CONFIG_FILE"; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${val}|" "$CONFIG_FILE"
|
||||||
|
else
|
||||||
|
echo "${key}=${val}" >>"$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "${key}=${val}" >"$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
chmod 600 "$CONFIG_FILE"
|
||||||
|
echo "[+] $key saved to $CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
--send)
|
||||||
|
shift
|
||||||
|
[ $# -ge 1 ] || usage
|
||||||
|
text="$1"
|
||||||
|
shift
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--token) TELEGRAM_BOT_TOKEN="$2"; shift 2 ;;
|
||||||
|
--chat-id) TELEGRAM_CHAT_ID="$2"; shift 2 ;;
|
||||||
|
*) err "Unknown option '$1'" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
load_config
|
||||||
|
cmd_send "$text"
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
load_config
|
||||||
|
cmd_send "Test message from pos $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
;;
|
||||||
|
config)
|
||||||
|
shift
|
||||||
|
case "${1:-}" in
|
||||||
|
set) shift; [ $# -eq 1 ] || usage; cmd_config_set "$1" ;;
|
||||||
|
*) cmd_config ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown telegram command '$cmd'"
|
||||||
|
echo "Run 'pos communication telegram --help' for usage."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: docker compose — Docker Compose service manager (ls/up/down/restart/logs/update/config)
|
||||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
SCALE_DIR="/usr/local/share/linux_post_install/scale-tail/services"
|
SCALE_DIR="/usr/local/share/linux_post_install/scale-tail/services"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: docker health — One-glance container health dashboard (exits 1 if unhealthy)
|
||||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: docker ps — Enhanced container overview (health, IPs, ports, uptime)
|
||||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: docker vbox — Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
|
||||||
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage:
|
Usage:
|
||||||
pos vbox create <name> [image] [--dir <path>]
|
pos docker vbox create <name> [image] [--dir <path>]
|
||||||
pos vbox enter <name>
|
pos docker vbox enter <name>
|
||||||
pos vbox stop <name>
|
pos docker vbox stop <name>
|
||||||
pos vbox start <name>
|
pos docker vbox start <name>
|
||||||
pos vbox rm <name>
|
pos docker vbox rm <name>
|
||||||
pos vbox ls
|
pos docker vbox ls
|
||||||
|
|
||||||
Manage disposable Docker containers as lightweight VMs.
|
Manage disposable Docker containers as lightweight VMs.
|
||||||
|
|
||||||
@@ -22,15 +23,15 @@ Options:
|
|||||||
Use "." for current directory
|
Use "." for current directory
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
pos vbox create lab1
|
pos docker vbox create lab1
|
||||||
pos vbox create lab1 --dir .
|
pos docker vbox create lab1 --dir .
|
||||||
pos vbox create lab1 --dir /mnt/data/lab1
|
pos docker vbox create lab1 --dir /mnt/data/lab1
|
||||||
pos vbox create kali kalilinux/kali-rolling
|
pos docker vbox create kali kalilinux/kali-rolling
|
||||||
pos vbox enter lab1
|
pos docker vbox enter lab1
|
||||||
pos vbox stop lab1
|
pos docker vbox stop lab1
|
||||||
pos vbox start lab1
|
pos docker vbox start lab1
|
||||||
pos vbox rm lab1
|
pos docker vbox rm lab1
|
||||||
pos vbox ls
|
pos docker vbox ls
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#This pos-entertaiment bin for configure enable or disable entertaiments elements such enable/disable weather, gold ...so on
|
||||||
|
# and check flags to choose wich entertaiments to run or not
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="$HOME/.config/linux_post_install"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/entertaiment.env"
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: media mp3 — Download audio as MP3 (yt-dlp)
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: media mp4 — Download video as MP4 (interactive format select)
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: network checkport — Check TCP port connectivity
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
Executable
+93
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# POS: network hotspot — Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||||
|
# POS_FLAGS: --foreground
|
||||||
|
|
||||||
|
LOGFILE=/var/log/linux_post_install_hotspot.log
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos network hotspot [command] [args]
|
||||||
|
|
||||||
|
Manage Wi-Fi hotspots with create_ap (CLI) or wihotspot-gui (GUI).
|
||||||
|
These tools are precompiled and shipped in x64_bin/ (or arm64_bin/),
|
||||||
|
installed to /usr/local/bin/ by install.sh.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
(no args) Launch the wihotspot-gui
|
||||||
|
start [--foreground] <wifi-iface> [<internet-iface>] <ssid> [<passphrase>]
|
||||||
|
Create an access point; asks whether to run in the
|
||||||
|
background. Use --foreground to skip the prompt
|
||||||
|
and block until Ctrl+C.
|
||||||
|
stop [<id>] Stop the running access point. <id> is an interface
|
||||||
|
name or PID; if omitted it is detected automatically.
|
||||||
|
status List running access points
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos network hotspot
|
||||||
|
pos network hotspot start wlan0 eth0 MyNet mypass
|
||||||
|
pos network hotspot start --foreground wlan0 eth0 MyNet mypass
|
||||||
|
pos network hotspot stop
|
||||||
|
pos network hotspot status
|
||||||
|
|
||||||
|
Background logs: $LOGFILE
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
"") exec wihotspot-gui ;;
|
||||||
|
start)
|
||||||
|
shift
|
||||||
|
fg=0
|
||||||
|
bg=0
|
||||||
|
if [ "${1:-}" = "--foreground" ]; then
|
||||||
|
fg=1
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
[ $# -ge 2 ] || usage
|
||||||
|
if [ $fg -eq 0 ]; then
|
||||||
|
read -rp "Run in background? [y/N]: " bg
|
||||||
|
case "$bg" in
|
||||||
|
[yY]*) bg=1 ;;
|
||||||
|
*) bg=0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [ $bg -eq 1 ]; then
|
||||||
|
sudo create_ap --daemon --logfile "$LOGFILE" "$@"
|
||||||
|
echo "[+] Hotspot started in background"
|
||||||
|
echo " Log : $LOGFILE"
|
||||||
|
echo " Status: pos network hotspot status"
|
||||||
|
echo " Stop : pos network hotspot stop"
|
||||||
|
else
|
||||||
|
exec sudo create_ap "$@"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
shift
|
||||||
|
id="${1:-}"
|
||||||
|
if [ -z "$id" ]; then
|
||||||
|
ids=$(sudo create_ap --list-running | awk 'NF { print $2 }')
|
||||||
|
if [ -z "$ids" ]; then
|
||||||
|
echo "No hotspot running."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for id in $ids; do
|
||||||
|
sudo create_ap --stop "$id"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
sudo create_ap --stop "$id"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
exec sudo create_ap --list-running
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unknown hotspot command '$cmd'"
|
||||||
|
echo "Run 'pos network hotspot --help' for usage."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
+24
-1
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: network ip — Show interfaces, routes, public IP + location
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -42,5 +43,27 @@ SEP
|
|||||||
echo "Public IP"
|
echo "Public IP"
|
||||||
SEP
|
SEP
|
||||||
|
|
||||||
curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || echo "Unavailable"
|
PUBLIC_IP=$(curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null) || true
|
||||||
|
|
||||||
|
if [ -n "${PUBLIC_IP:-}" ]; then
|
||||||
|
echo "$PUBLIC_IP"
|
||||||
|
GEO=$(curl -4 -s --max-time 5 "http://ip-api.com/json/${PUBLIC_IP}?fields=status,country,regionName,city,isp" 2>/dev/null) || true
|
||||||
|
if [ -n "${GEO:-}" ]; then
|
||||||
|
location=$(echo "$GEO" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
if d.get('status') == 'success':
|
||||||
|
print(f\"{d['city']}, {d['regionName']}, {d['country']} ({d['isp']})\")
|
||||||
|
" 2>/dev/null) || true
|
||||||
|
if [ -n "${location:-}" ]; then
|
||||||
|
printf "%-20s %s\n" "Location" "$location"
|
||||||
|
else
|
||||||
|
echo "Location: Unavailable"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Location: Unavailable"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Unavailable"
|
||||||
|
fi
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: network scan — Parallel ping sweep of CIDR
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: ssh load-keys — Load all SSH keys into the agent
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
|
|||||||
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# POS: system backup — Encrypted (AES-256) folder snapshots (tar + gpg)
|
||||||
|
# POS_FLAGS: --service
|
||||||
|
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos system backup <folder-path>
|
||||||
|
pos system backup --service
|
||||||
|
|
||||||
|
Create a gpg-encrypted (AES-256) tar.gz snapshot of a folder and verify it.
|
||||||
|
The archive password is prompted twice and never stored.
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
<folder-path> Back up that folder directly.
|
||||||
|
--service List folders under /srv and ~/srv, pick one, back it up.
|
||||||
|
|
||||||
|
The final artifact <name>_<date>.tar.gz.gpg is written to the current directory.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
BACKUP_SERVICE_ROOTS Space-separated roots for --service
|
||||||
|
(default: /srv \$HOME/srv)
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v tar &>/dev/null || err "tar not found"
|
||||||
|
command -v gpg &>/dev/null || err "gpg not found (install gnupg)"
|
||||||
|
|
||||||
|
SERVICE=0
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
--service) SERVICE=1 ;;
|
||||||
|
"") err "Missing folder path (or use --service)" ;;
|
||||||
|
*) FOLDER="$1" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$SERVICE" -eq 1 ]; then
|
||||||
|
if [ -n "${BACKUP_SERVICE_ROOTS:-}" ]; then
|
||||||
|
read -r -a roots <<< "$BACKUP_SERVICE_ROOTS"
|
||||||
|
else
|
||||||
|
roots=(/srv "$HOME/srv")
|
||||||
|
fi
|
||||||
|
|
||||||
|
idx=0
|
||||||
|
for root in "${roots[@]}"; do
|
||||||
|
[ -d "$root" ] || { warn "Root not found: $root"; continue; }
|
||||||
|
|
||||||
|
dirs=()
|
||||||
|
while IFS= read -r d; do
|
||||||
|
dirs+=("$d")
|
||||||
|
done < <(find "$root" -maxdepth 1 -mindepth 1 -type d | sort)
|
||||||
|
|
||||||
|
[ ${#dirs[@]} -gt 0 ] || { warn "No folders in $root"; continue; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "${root}:"
|
||||||
|
for d in "${dirs[@]}"; do
|
||||||
|
idx=$((idx + 1))
|
||||||
|
names[$idx]="$d"
|
||||||
|
printf "%2d) %s\n" "$idx" "$d"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "$idx" -gt 0 ] || err "No folders found under: ${roots[*]}"
|
||||||
|
|
||||||
|
read -rp "Select folder number: " choice
|
||||||
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > idx )); then
|
||||||
|
err "Invalid selection: $choice"
|
||||||
|
fi
|
||||||
|
FOLDER="${names[$choice]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -d "$FOLDER" ] || err "Folder not found: $FOLDER"
|
||||||
|
|
||||||
|
NAME="$(basename "$FOLDER")"
|
||||||
|
DATE="$(date +%Y-%m-%d_%H-%M-%S)"
|
||||||
|
ARCHIVE="${NAME}_${DATE}.tar.gz"
|
||||||
|
|
||||||
|
echo
|
||||||
|
log "Creating backup..."
|
||||||
|
echo "Source : $FOLDER"
|
||||||
|
echo "Output : $ARCHIVE"
|
||||||
|
|
||||||
|
sudo tar -czvf "$ARCHIVE" -C "$(dirname "$FOLDER")" "$NAME"
|
||||||
|
|
||||||
|
log "Verifying archive..."
|
||||||
|
tar -tzf "$ARCHIVE" > /dev/null
|
||||||
|
log "Archive verified"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -s -rp "Enter backup password: " PASS
|
||||||
|
echo
|
||||||
|
read -s -rp "Confirm backup password: " CONFIRM
|
||||||
|
echo
|
||||||
|
if [ -n "$PASS" ] && [ "$PASS" = "$CONFIRM" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
warn "Passwords are empty or do not match — try again"
|
||||||
|
done
|
||||||
|
unset CONFIRM
|
||||||
|
|
||||||
|
log "Encrypting backup..."
|
||||||
|
gpg --batch --yes --passphrase "$PASS" --symmetric --cipher-algo AES256 "$ARCHIVE"
|
||||||
|
|
||||||
|
rm -f "$ARCHIVE"
|
||||||
|
ARCHIVE="${ARCHIVE}.gpg"
|
||||||
|
chmod 600 "$ARCHIVE"
|
||||||
|
|
||||||
|
log "Verifying encrypted backup..."
|
||||||
|
gpg --batch --quiet --passphrase "$PASS" --decrypt "$ARCHIVE" | tar -tzf - > /dev/null
|
||||||
|
|
||||||
|
unset PASS
|
||||||
|
echo
|
||||||
|
log "Backup completed: $ARCHIVE"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
# POS: system firewall — Interactive UFW management
|
||||||
IFS=$'\n\t'
|
IFS=$'\n\t'
|
||||||
|
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
|||||||
Executable
+218
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# POS: usb server — USB Redirector server control (--ls, --share; prompts when args omitted)
|
||||||
|
# POS_FLAGS: --ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos usb server [flag] [args]
|
||||||
|
|
||||||
|
Control the USB Redirector server (usbsrv) — share local USB devices
|
||||||
|
over the network and manage connected clients.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--ls List host USB devices and connected clients
|
||||||
|
--ls-shared List shared or in-use devices only
|
||||||
|
--share [dev-id] [client-id] Share a device and connect it to a client
|
||||||
|
(interactive picker when IDs are omitted)
|
||||||
|
--unshare [dev-id] Stop sharing a device
|
||||||
|
--auto-share on|off Toggle automatic sharing of new devices
|
||||||
|
--callback [addr:port] Create a callback connection to a client
|
||||||
|
--close-callback [target|all] Close a client callback
|
||||||
|
--auto-connect on|off [client] Toggle remote auto-connect for a client
|
||||||
|
--disconnect [dev-id|all] Disconnect a device from its clients
|
||||||
|
--nickname [dev-id] [nick] Set a device nickname (empty nick removes it)
|
||||||
|
--timeout [dev-id] [sec] Set device inactivity timeout (0 disables)
|
||||||
|
--port [num] Set the TCP port (restart server to apply)
|
||||||
|
--info Show server info
|
||||||
|
--version Show server version
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos usb server --ls
|
||||||
|
pos usb server --share
|
||||||
|
pos usb server --share 0-1 3
|
||||||
|
pos usb server --auto-share on
|
||||||
|
pos usb server --port 32032
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_ls() {
|
||||||
|
echo "==== HOST USB DEVICES ===="
|
||||||
|
usbsrv -list-devices
|
||||||
|
echo
|
||||||
|
echo "==== CONNECTED CLIENTS ===="
|
||||||
|
usbsrv -list-clients
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_ls_shared() {
|
||||||
|
echo "==== SHARED OR IN USE DEVICES ===="
|
||||||
|
usbsrv -list-devices | awk '
|
||||||
|
BEGIN {RS=""; FS="\n"}
|
||||||
|
{
|
||||||
|
for(i=1; i<=NF; i++) {
|
||||||
|
if ($i ~ /Status:.*(shared|in use)/) {
|
||||||
|
print $0 "\n"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_share() {
|
||||||
|
local dev="${1:-}" client="${2:-}"
|
||||||
|
if [ -z "$dev" ]; then
|
||||||
|
echo "==== HOST USB DEVICES ===="
|
||||||
|
usbsrv -list-devices
|
||||||
|
echo
|
||||||
|
echo "==== CONNECTED CLIENTS ===="
|
||||||
|
usbsrv -list-clients
|
||||||
|
echo
|
||||||
|
read -rp "Enter device ID to share: " dev
|
||||||
|
read -rp "Enter client ID to connect to: " client
|
||||||
|
fi
|
||||||
|
[ -n "$dev" ] || err "no device ID given"
|
||||||
|
[ -n "$client" ] || err "no client ID given"
|
||||||
|
log "Sharing device ID $dev..."
|
||||||
|
usbsrv -share "$dev"
|
||||||
|
log "Connecting device $dev to client $client..."
|
||||||
|
usbsrv -connect-to "${client}-${dev}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_unshare() {
|
||||||
|
local dev="${1:-}"
|
||||||
|
if [ -z "$dev" ]; then
|
||||||
|
cmd_ls_shared
|
||||||
|
read -rp "Enter device ID to unshare: " dev
|
||||||
|
fi
|
||||||
|
[ -n "$dev" ] || err "no device ID given"
|
||||||
|
log "Unsharing device ID $dev..."
|
||||||
|
usbsrv -unshare "$dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_auto_share() {
|
||||||
|
local mode="${1:-}"
|
||||||
|
if [ -z "$mode" ]; then
|
||||||
|
read -rp "Turn auto-share on or off? (on/off): " mode
|
||||||
|
fi
|
||||||
|
case "$mode" in
|
||||||
|
on|off) ;;
|
||||||
|
*) err "Invalid option '$mode' — must be 'on' or 'off'" ;;
|
||||||
|
esac
|
||||||
|
usbsrv -auto-share "$mode"
|
||||||
|
log "Auto-share set to $mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_callback() {
|
||||||
|
local addr="${1:-}"
|
||||||
|
if [ -z "$addr" ]; then
|
||||||
|
read -rp "Enter client address:port for callback (e.g. 192.168.1.100:32032): " addr
|
||||||
|
fi
|
||||||
|
[ -n "$addr" ] || err "no address given"
|
||||||
|
usbsrv -create-callback "$addr"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_close_callback() {
|
||||||
|
local target="${1:-}"
|
||||||
|
if [ -z "$target" ]; then
|
||||||
|
usbsrv -list-clients
|
||||||
|
read -rp "Enter client or clientid or 'all' to close callback: " target
|
||||||
|
fi
|
||||||
|
[ -n "$target" ] || err "no target given"
|
||||||
|
usbsrv -close-callback "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_auto_connect() {
|
||||||
|
local mode="${1:-}" client="${2:-}"
|
||||||
|
if [ -z "$mode" ]; then
|
||||||
|
read -rp "Enable or disable remote auto-connect? (on/off): " mode
|
||||||
|
fi
|
||||||
|
case "$mode" in
|
||||||
|
on|off) ;;
|
||||||
|
*) err "Invalid option '$mode' — must be 'on' or 'off'" ;;
|
||||||
|
esac
|
||||||
|
if [ -z "$client" ]; then
|
||||||
|
usbsrv -list-clients
|
||||||
|
read -rp "Enter client or clientid: " client
|
||||||
|
fi
|
||||||
|
[ -n "$client" ] || err "no client given"
|
||||||
|
usbsrv -remote-auto-connect "$mode" "$client"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_disconnect() {
|
||||||
|
local dev="${1:-}"
|
||||||
|
if [ -z "$dev" ]; then
|
||||||
|
cmd_ls_shared
|
||||||
|
read -rp "Enter device ID or 'all' to disconnect from clients: " dev
|
||||||
|
fi
|
||||||
|
[ -n "$dev" ] || err "no device ID given"
|
||||||
|
usbsrv -disconnect-from "$dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_nickname() {
|
||||||
|
local dev="${1:-}" nick="${2:-}"
|
||||||
|
if [ -z "$dev" ]; then
|
||||||
|
usbsrv -list-devices
|
||||||
|
read -rp "Enter device ID to set nickname (empty to remove): " dev
|
||||||
|
read -rp "Enter nickname (or leave empty to remove): " nick
|
||||||
|
fi
|
||||||
|
[ -n "$dev" ] || err "no device ID given"
|
||||||
|
usbsrv -set-nickname "$nick" "$dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_timeout() {
|
||||||
|
local dev="${1:-}" sec="${2:-}"
|
||||||
|
if [ -z "$dev" ]; then
|
||||||
|
usbsrv -list-devices
|
||||||
|
read -rp "Enter device ID to set timeout for: " dev
|
||||||
|
read -rp "Enter timeout in seconds (0 to disable): " sec
|
||||||
|
fi
|
||||||
|
[ -n "$dev" ] || err "no device ID given"
|
||||||
|
[[ "$sec" =~ ^[0-9]+$ ]] || err "Invalid timeout '$sec' — must be a number"
|
||||||
|
usbsrv -set-timeout "$sec" "$dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_port() {
|
||||||
|
local port="${1:-}"
|
||||||
|
if [ -z "$port" ]; then
|
||||||
|
read -rp "Enter new TCP port: " port
|
||||||
|
fi
|
||||||
|
[[ "$port" =~ ^[0-9]+$ ]] || err "Invalid port number '$port'"
|
||||||
|
usbsrv -set-tcp-port "$port"
|
||||||
|
warn "Restart your server for port changes to take effect."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
--ls|--ls-shared|--share|--unshare|--auto-share|--callback|--close-callback|--auto-connect|--disconnect|--nickname|--timeout|--port|--info|--version) ;;
|
||||||
|
*) err "Unknown flag '$cmd'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
command -v usbsrv &>/dev/null \
|
||||||
|
|| err "usbsrv not found — install the USB Redirector server (https://www.incentivespro.com/usb-server.html)"
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
--ls) cmd_ls ;;
|
||||||
|
--ls-shared) cmd_ls_shared ;;
|
||||||
|
--share) shift; cmd_share "$@" ;;
|
||||||
|
--unshare) shift; cmd_unshare "$@" ;;
|
||||||
|
--auto-share) shift; cmd_auto_share "$@" ;;
|
||||||
|
--callback) shift; cmd_callback "$@" ;;
|
||||||
|
--close-callback) shift; cmd_close_callback "$@" ;;
|
||||||
|
--auto-connect) shift; cmd_auto_connect "$@" ;;
|
||||||
|
--disconnect) shift; cmd_disconnect "$@" ;;
|
||||||
|
--nickname) shift; cmd_nickname "$@" ;;
|
||||||
|
--timeout) shift; cmd_timeout "$@" ;;
|
||||||
|
--port) shift; cmd_port "$@" ;;
|
||||||
|
--info) usbsrv -info ;;
|
||||||
|
--version) usbsrv -version ;;
|
||||||
|
*) err "Unknown flag '$cmd'"; usage ;;
|
||||||
|
esac
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
exec pos vbox "$@"
|
exec pos docker vbox "$@"
|
||||||
|
|||||||
+34
-6
@@ -1,6 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Bash completion for pos — dynamically discovers pos-* subcommands
|
# Bash completion for pos — dynamically discovers pos-* subcommands
|
||||||
# Install: source this file in ~/.bashrc or place in /etc/bash_completion.d/
|
# Install: source this file in ~/.bashrc or place in /etc/bash_completion.d/
|
||||||
|
# GEN:START posflags
|
||||||
|
declare -A _pos_flags
|
||||||
|
_pos_flags[communication-telegram]="--send"
|
||||||
|
_pos_flags[network-hotspot]="--foreground"
|
||||||
|
_pos_flags[system-backup]="--service"
|
||||||
|
_pos_flags[usb-server]="--ls --ls-shared --share --unshare --auto-share --callback --close-callback --auto-connect --disconnect --nickname --timeout --port --info --version"
|
||||||
|
# GEN:END posflags
|
||||||
|
|
||||||
_pos() {
|
_pos() {
|
||||||
local cur prev words cword
|
local cur prev words cword
|
||||||
@@ -45,7 +52,7 @@ _pos() {
|
|||||||
|
|
||||||
_pos_complete_subcats() {
|
_pos_complete_subcats() {
|
||||||
local cat="${words[1]}"
|
local cat="${words[1]}"
|
||||||
COMPREPLY=($(compgen -W "${cat_cmds[$cat]:-}" -- "$cur"))
|
COMPREPLY=($(compgen -W "${cat_cmds[$cat]:-} --help" -- "$cur"))
|
||||||
}
|
}
|
||||||
|
|
||||||
_pos_complete_compose_services() {
|
_pos_complete_compose_services() {
|
||||||
@@ -63,7 +70,7 @@ _pos() {
|
|||||||
COMPREPLY=($(compgen -W "ls installed up down restart logs update config" -- "$cur"))
|
COMPREPLY=($(compgen -W "ls installed up down restart logs update config" -- "$cur"))
|
||||||
}
|
}
|
||||||
|
|
||||||
_pos_complete_vbox_cmds() {
|
_pos_complete_docker_vbox_cmds() {
|
||||||
COMPREPLY=($(compgen -W "create enter stop start rm ls" -- "$cur"))
|
COMPREPLY=($(compgen -W "create enter stop start rm ls" -- "$cur"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +80,11 @@ _pos() {
|
|||||||
COMPREPLY=($(compgen -W "$names" -- "$cur"))
|
COMPREPLY=($(compgen -W "$names" -- "$cur"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pos_complete_flags() {
|
||||||
|
local tool="$1"
|
||||||
|
COMPREPLY=($(compgen -W "${_pos_flags[$tool]:-} --help" -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
# ── Dispatch ───────────────────────────────────────────────
|
# ── Dispatch ───────────────────────────────────────────────
|
||||||
case "${#words[@]}" in
|
case "${#words[@]}" in
|
||||||
2)
|
2)
|
||||||
@@ -93,8 +105,20 @@ _pos() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
vbox-*)
|
docker-vbox)
|
||||||
_pos_complete_vbox_cmds
|
_pos_complete_docker_vbox_cmds
|
||||||
|
;;
|
||||||
|
usb-server)
|
||||||
|
_pos_complete_flags usb-server
|
||||||
|
;;
|
||||||
|
communication-telegram)
|
||||||
|
_pos_complete_flags communication-telegram
|
||||||
|
;;
|
||||||
|
network-hotspot)
|
||||||
|
_pos_complete_flags network-hotspot
|
||||||
|
;;
|
||||||
|
system-backup)
|
||||||
|
_pos_complete_flags system-backup
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
@@ -107,8 +131,12 @@ _pos() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
vbox-create|vbox-enter|vbox-stop|vbox-start|vbox-rm)
|
docker-vbox)
|
||||||
_pos_complete_docker_vbox_names
|
case "${words[3]}" in
|
||||||
|
create|enter|stop|start|rm)
|
||||||
|
_pos_complete_docker_vbox_names
|
||||||
|
;;
|
||||||
|
esac
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK8u4mvN1oWUGjWZCjUT0742u6AJEIHfi+PAQgZuNnPv
|
|
||||||
|
|||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DATA=$(curl -s "https://api.coingecko.com/api/v3/simple/price?ids=pax-gold&vs_currencies=usd")
|
||||||
|
|
||||||
|
if [ -z "$DATA" ]; then
|
||||||
|
echo "Gold API unavailable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PRICE=$(echo "$DATA" | grep -o '"usd":[0-9.]*' | cut -d: -f2)
|
||||||
|
|
||||||
|
if [ -z "$PRICE" ]; then
|
||||||
|
echo "Parse error"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
GRAM=$(echo "scale=2; $PRICE / 31.1035" | bc)
|
||||||
|
|
||||||
|
MESSAGE="🪙 Gold Price
|
||||||
|
|
||||||
|
1 oz: $PRICE USD
|
||||||
|
1 gram: $GRAM USD"
|
||||||
|
|
||||||
|
echo "$MESSAGE"
|
||||||
|
|
||||||
|
pos communication telegram --send "$MESSAGE"
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# meed to be dynamic later
|
||||||
|
LAT="52.3676"
|
||||||
|
LON="4.9041"
|
||||||
|
|
||||||
|
DATA=$(curl -s "https://api.open-meteo.com/v1/forecast?latitude=$LAT&longitude=$LON¤t_weather=true")
|
||||||
|
|
||||||
|
TEMP=$(echo "$DATA" | grep -o '"temperature":[^,]*' | cut -d: -f2)
|
||||||
|
WIND=$(echo "$DATA" | grep -o '"windspeed":[^,]*' | cut -d: -f2)
|
||||||
|
|
||||||
|
MESSAGE="🌤 Weather
|
||||||
|
Temperature: ${TEMP}°C
|
||||||
|
Wind: ${WIND} km/h"
|
||||||
|
|
||||||
|
echo "$MESSAGE"
|
||||||
|
|
||||||
|
pos communication telegram --send "$MESSAGE"
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from gui.routes import routers
|
|
||||||
|
|
||||||
app = FastAPI(title="Linux_post_install")
|
|
||||||
|
|
||||||
static_dir = Path(__file__).parent / "static"
|
|
||||||
if static_dir.is_dir():
|
|
||||||
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
||||||
|
|
||||||
for router in routers:
|
|
||||||
app.include_router(router)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
return RedirectResponse(url="/dashboard")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
fastapi>=0.110.0
|
|
||||||
uvicorn[standard]>=0.29.0
|
|
||||||
jinja2>=3.1.0
|
|
||||||
python-multipart>=0.0.9
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from gui.routes.dashboard import router as dashboard_router
|
|
||||||
from gui.routes.install import router as install_router
|
|
||||||
from gui.routes.compose import router as compose_router
|
|
||||||
from gui.routes.network import router as network_router
|
|
||||||
from gui.routes.vbox import router as vbox_router
|
|
||||||
from gui.routes.system import router as system_router
|
|
||||||
from gui.routes.media import router as media_router
|
|
||||||
from gui.routes.ssh import router as ssh_router
|
|
||||||
|
|
||||||
routers = [
|
|
||||||
dashboard_router,
|
|
||||||
install_router,
|
|
||||||
compose_router,
|
|
||||||
network_router,
|
|
||||||
vbox_router,
|
|
||||||
system_router,
|
|
||||||
media_router,
|
|
||||||
ssh_router,
|
|
||||||
]
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, Request
|
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/compose")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_class=HTMLResponse)
|
|
||||||
async def compose_page(request: Request):
|
|
||||||
services = runner.list_services()
|
|
||||||
deployed = runner.list_deployed()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"compose/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "compose",
|
|
||||||
"services": services,
|
|
||||||
"deployed": deployed,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{service}", response_class=HTMLResponse)
|
|
||||||
async def service_detail(request: Request, service: str):
|
|
||||||
deployed = (Path("/srv") / service).is_dir()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"compose/detail.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "compose",
|
|
||||||
"service": service,
|
|
||||||
"deployed": deployed,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{service}/stream/{action}")
|
|
||||||
async def service_stream(service: str, action: str):
|
|
||||||
async def generate():
|
|
||||||
cmd_map = {
|
|
||||||
"up": f"pos docker compose up {service}",
|
|
||||||
"down": f"pos docker compose down {service}",
|
|
||||||
"restart": f"pos docker compose restart {service}",
|
|
||||||
"logs": f"pos docker compose logs {service}",
|
|
||||||
}
|
|
||||||
cmd = cmd_map.get(action)
|
|
||||||
if not cmd:
|
|
||||||
yield f"event: error\ndata: Unknown action: {action}\n\n"
|
|
||||||
return
|
|
||||||
async for event in runner.stream(cmd):
|
|
||||||
yield f"event: {event['event']}\ndata: {event['data']}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/config/set")
|
|
||||||
async def config_set(key: str = Form(...), value: str = Form(...)):
|
|
||||||
code, out, err = await runner.run(f"pos docker compose config set {key}={value}")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config", response_class=HTMLResponse)
|
|
||||||
async def config_page(request: Request):
|
|
||||||
code, out, _ = await runner.run("pos docker compose config")
|
|
||||||
config_file = Path.home() / ".config" / "linux_post_install" / "compose.env"
|
|
||||||
current = ""
|
|
||||||
if config_file.is_file():
|
|
||||||
current = config_file.read_text()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"compose/config.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "compose",
|
|
||||||
"current": current,
|
|
||||||
"status": out,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from fastapi import APIRouter, Request
|
|
||||||
from fastapi.responses import HTMLResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/dashboard", response_class=HTMLResponse)
|
|
||||||
async def dashboard(request: Request):
|
|
||||||
docker_ok = bool(runner.script_path("docker"))
|
|
||||||
compose_count = len(runner.list_services())
|
|
||||||
deployed_count = len(runner.list_deployed())
|
|
||||||
app_categories = runner.list_app_categories()
|
|
||||||
total_apps = sum(len(apps) for apps in app_categories.values())
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"dashboard.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "dashboard",
|
|
||||||
"docker_ok": docker_ok,
|
|
||||||
"compose_count": compose_count,
|
|
||||||
"deployed_count": deployed_count,
|
|
||||||
"total_apps": total_apps,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Request
|
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/install")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_class=HTMLResponse)
|
|
||||||
async def install_page(request: Request):
|
|
||||||
app_categories = runner.list_app_categories()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"install/index.html",
|
|
||||||
{"request": request, "active": "install", "app_categories": app_categories},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stream")
|
|
||||||
async def install_stream(phases: str = "1,2,3,4", apps: str = ""):
|
|
||||||
phase_map = {
|
|
||||||
"1": ("preinstall", False),
|
|
||||||
"2": ("scripts", True),
|
|
||||||
"3": ("postinstall", False),
|
|
||||||
"4": ("scalepoint", True),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def event_stream():
|
|
||||||
repo = Path(__file__).resolve().parents[2]
|
|
||||||
|
|
||||||
selected = [p.strip() for p in phases.split(",") if p.strip() in phase_map]
|
|
||||||
|
|
||||||
for phase_num in selected:
|
|
||||||
name, needs_sudo = phase_map[phase_num]
|
|
||||||
script = phase_map[phase_num][0]
|
|
||||||
yield {"event": "phase_start", "data": f"Phase {phase_num}: {script}"}
|
|
||||||
|
|
||||||
if needs_sudo:
|
|
||||||
gen = runner.stream_sudo(f"bash {repo}/install.sh --steps {phase_num}")
|
|
||||||
else:
|
|
||||||
gen = runner.stream(f"bash {repo}/install.sh --steps {phase_num}")
|
|
||||||
|
|
||||||
async for event in gen:
|
|
||||||
yield event
|
|
||||||
|
|
||||||
if apps:
|
|
||||||
app_list = apps.split(",")
|
|
||||||
for app in app_list:
|
|
||||||
app = app.strip()
|
|
||||||
if not app:
|
|
||||||
continue
|
|
||||||
yield {"event": "phase_start", "data": f"Installing app: {app}"}
|
|
||||||
category, name = app.split("/", 1)
|
|
||||||
script = repo / "apps" / category / f"{name}.sh"
|
|
||||||
if script.is_file():
|
|
||||||
async for event in runner.stream_sudo(f"bash {script}"):
|
|
||||||
yield event
|
|
||||||
else:
|
|
||||||
yield {"event": "error", "data": f"App script not found: {app}"}
|
|
||||||
|
|
||||||
yield {"event": "exit", "data": "0"}
|
|
||||||
|
|
||||||
async def generate():
|
|
||||||
async for event in event_stream():
|
|
||||||
yield f"event: {event['event']}\ndata: {event['data']}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
from fastapi import APIRouter, Form, Request
|
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/media")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_class=HTMLResponse)
|
|
||||||
async def media_page(request: Request):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"media/index.html", {"request": request, "active": "media"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/mp3/stream")
|
|
||||||
async def media_mp3_stream(url: str = Form(...)):
|
|
||||||
async def generate():
|
|
||||||
async for event in runner.stream(f"pos media mp3 {url}"):
|
|
||||||
yield f"event: {event['event']}\ndata: {event['data']}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/mp4/formats")
|
|
||||||
async def media_mp4_formats(url: str = Form(...)):
|
|
||||||
code, out, err = await runner.run(f"yt-dlp -F {url}")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/mp4/stream")
|
|
||||||
async def media_mp4_stream(url: str = Form(...), format_id: str = Form(...)):
|
|
||||||
async def generate():
|
|
||||||
async for event in runner.stream(
|
|
||||||
f"yt-dlp -f {format_id} --merge-output-format mp4 --embed-thumbnail --add-metadata -o '~/Videos/%(title)s.%(ext)s' {url}"
|
|
||||||
):
|
|
||||||
yield f"event: {event['event']}\ndata: {event['data']}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
from fastapi import APIRouter, Form, Query, Request
|
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/network")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_class=HTMLResponse)
|
|
||||||
async def network_page(request: Request):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"network/index.html", {"request": request, "active": "network"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ip", response_class=HTMLResponse)
|
|
||||||
async def network_ip(request: Request):
|
|
||||||
code, out, err = await runner.run("pos network ip")
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"network/ip.html",
|
|
||||||
{"request": request, "active": "network", "output": out or err, "code": code},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/checkport", response_class=HTMLResponse)
|
|
||||||
async def network_checkport(request: Request, target: str = Form(...)):
|
|
||||||
code, out, err = await runner.run(f"pos network checkport {target}")
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"network/checkport.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "network",
|
|
||||||
"output": out or err,
|
|
||||||
"code": code,
|
|
||||||
"target": target,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scan", response_class=HTMLResponse)
|
|
||||||
async def network_scan_page(request: Request):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"network/scan.html", {"request": request, "active": "network"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/scan/stream")
|
|
||||||
async def network_scan_stream(
|
|
||||||
target: str = Form(...), full: bool = Form(False), retries: int = Form(1)
|
|
||||||
):
|
|
||||||
async def generate():
|
|
||||||
cmd = f"pos network scan {target}"
|
|
||||||
if full:
|
|
||||||
cmd += " --full"
|
|
||||||
if retries > 1:
|
|
||||||
cmd += f" --retries {retries}"
|
|
||||||
async for event in runner.stream(cmd):
|
|
||||||
yield f"event: {event['event']}\ndata: {event['data']}\n\n"
|
|
||||||
|
|
||||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from fastapi import APIRouter, Request
|
|
||||||
from fastapi.responses import HTMLResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/ssh")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_class=HTMLResponse)
|
|
||||||
async def ssh_page(request: Request):
|
|
||||||
code, out, err = await runner.run("pos ssh load-keys")
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"ssh/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "ssh",
|
|
||||||
"output": out or err,
|
|
||||||
"code": code,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/load")
|
|
||||||
async def ssh_load():
|
|
||||||
code, out, err = await runner.run("pos ssh load-keys")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
from fastapi import APIRouter, Form, Request
|
|
||||||
from fastapi.responses import HTMLResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/system")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/firewall", response_class=HTMLResponse)
|
|
||||||
async def firewall_page(request: Request):
|
|
||||||
code, out, _ = await runner.run("sudo ufw status verbose")
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"system/firewall.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "system",
|
|
||||||
"status": out,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/firewall/enable")
|
|
||||||
async def firewall_enable():
|
|
||||||
code, out, err = await runner.run("sudo ufw --force enable")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/firewall/disable")
|
|
||||||
async def firewall_disable():
|
|
||||||
code, out, err = await runner.run("sudo ufw disable")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/firewall/allow")
|
|
||||||
async def firewall_allow(port: str = Form(...), protocol: str = Form("tcp")):
|
|
||||||
code, out, err = await runner.run(f"sudo ufw allow {port}/{protocol}")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/firewall/deny")
|
|
||||||
async def firewall_deny(port: str = Form(...), protocol: str = Form("tcp")):
|
|
||||||
code, out, err = await runner.run(f"sudo ufw deny {port}/{protocol}")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/firewall/delete")
|
|
||||||
async def firewall_delete(rule_num: str = Form(...)):
|
|
||||||
code, out, err = await runner.run(f"echo {rule_num} | sudo ufw delete")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/firewall/status")
|
|
||||||
async def firewall_status():
|
|
||||||
code, out, err = await runner.run("sudo ufw status verbose")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from fastapi import APIRouter, Form, Request
|
|
||||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
||||||
|
|
||||||
from gui.state import runner, templates
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/vbox")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_class=HTMLResponse)
|
|
||||||
async def vbox_page(request: Request):
|
|
||||||
code, out, err = await runner.run("pos vbox ls")
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"vbox/index.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"active": "vbox",
|
|
||||||
"containers": out or err,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/create")
|
|
||||||
async def vbox_create(
|
|
||||||
name: str = Form(...), image: str = Form("ubuntu:22.04"), directory: str = Form("")
|
|
||||||
):
|
|
||||||
cmd = f"pos vbox create {name} {image}"
|
|
||||||
if directory:
|
|
||||||
cmd += f" --dir {directory}"
|
|
||||||
code, out, err = await runner.run(cmd)
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{action}/{name}")
|
|
||||||
async def vbox_action(action: str, name: str):
|
|
||||||
if action not in ("start", "stop", "rm", "enter"):
|
|
||||||
return {"ok": False, "output": f"Unknown action: {action}"}
|
|
||||||
code, out, err = await runner.run(f"pos vbox {action} {name}")
|
|
||||||
return {"ok": code == 0, "output": out or err}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import shutil
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import AsyncGenerator, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class StreamEvent(str, Enum):
|
|
||||||
STDOUT = "stdout"
|
|
||||||
STDERR = "stderr"
|
|
||||||
EXIT = "exit"
|
|
||||||
ERROR = "error"
|
|
||||||
|
|
||||||
|
|
||||||
class PosRunner:
|
|
||||||
def __init__(self, repo_root: Optional[Path] = None):
|
|
||||||
self.repo_root = repo_root or Path(__file__).resolve().parents[2]
|
|
||||||
self.bin_dir = self.repo_root / "bin"
|
|
||||||
|
|
||||||
def _find_script(self, command: str) -> Optional[Path]:
|
|
||||||
parts = command.split()
|
|
||||||
name = f"pos-{'-'.join(parts)}"
|
|
||||||
candidate = self.bin_dir / name
|
|
||||||
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
||||||
return candidate
|
|
||||||
system = shutil.which(name)
|
|
||||||
if system:
|
|
||||||
return Path(system)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _resolve_command(self, command: str) -> list[str]:
|
|
||||||
script = self._find_script(command)
|
|
||||||
if script:
|
|
||||||
return [str(script)]
|
|
||||||
if command.startswith("sudo "):
|
|
||||||
return shlex.split(command)
|
|
||||||
return shlex.split(command)
|
|
||||||
|
|
||||||
async def run(
|
|
||||||
self,
|
|
||||||
command: str,
|
|
||||||
cwd: Optional[Path] = None,
|
|
||||||
env: Optional[dict[str, str]] = None,
|
|
||||||
) -> tuple[int, str, str]:
|
|
||||||
cmd = self._resolve_command(command)
|
|
||||||
merged = {**os.environ, **(env or {})}
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
cwd=cwd or self.repo_root,
|
|
||||||
env=merged,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
stdout, stderr = await proc.communicate()
|
|
||||||
return proc.returncode or 0, stdout.decode(), stderr.decode()
|
|
||||||
|
|
||||||
async def stream(
|
|
||||||
self,
|
|
||||||
command: str,
|
|
||||||
cwd: Optional[Path] = None,
|
|
||||||
env: Optional[dict[str, str]] = None,
|
|
||||||
) -> AsyncGenerator[dict, None]:
|
|
||||||
cmd = self._resolve_command(command)
|
|
||||||
merged = {**os.environ, **(env or {})}
|
|
||||||
try:
|
|
||||||
proc = await asyncio.create_subprocess_exec(
|
|
||||||
*cmd,
|
|
||||||
cwd=cwd or self.repo_root,
|
|
||||||
env=merged,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
|
||||||
yield {"event": StreamEvent.ERROR, "data": f"Command not found: {command}"}
|
|
||||||
return
|
|
||||||
|
|
||||||
async def read_stream(stream, event_type: StreamEvent):
|
|
||||||
while True:
|
|
||||||
line = await stream.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
yield {"event": event_type, "data": line.decode(errors="replace").rstrip()}
|
|
||||||
|
|
||||||
async for line in read_stream(proc.stdout, StreamEvent.STDOUT):
|
|
||||||
yield line
|
|
||||||
async for line in read_stream(proc.stderr, StreamEvent.STDERR):
|
|
||||||
yield line
|
|
||||||
|
|
||||||
code = await proc.wait()
|
|
||||||
yield {"event": StreamEvent.EXIT, "data": str(code)}
|
|
||||||
|
|
||||||
async def stream_sudo(
|
|
||||||
self,
|
|
||||||
command: str,
|
|
||||||
cwd: Optional[Path] = None,
|
|
||||||
env: Optional[dict[str, str]] = None,
|
|
||||||
) -> AsyncGenerator[dict, None]:
|
|
||||||
async for event in self.stream(f"sudo {command}", cwd=cwd, env=env):
|
|
||||||
yield event
|
|
||||||
|
|
||||||
def script_path(self, name: str) -> Optional[str]:
|
|
||||||
p = self.bin_dir / name
|
|
||||||
if p.is_file():
|
|
||||||
return str(p)
|
|
||||||
system = shutil.which(name)
|
|
||||||
if system:
|
|
||||||
return system
|
|
||||||
return None
|
|
||||||
|
|
||||||
def list_services(self) -> list[str]:
|
|
||||||
scale_dir = Path("/usr/local/share/linux_post_install/scale-tail/services")
|
|
||||||
if not scale_dir.is_dir():
|
|
||||||
return []
|
|
||||||
return sorted(d.name for d in scale_dir.iterdir() if d.is_dir())
|
|
||||||
|
|
||||||
def list_deployed(self) -> list[str]:
|
|
||||||
services_base = os.environ.get("SERVICES_BASE", "/srv")
|
|
||||||
base = Path(services_base)
|
|
||||||
if not base.is_dir():
|
|
||||||
return []
|
|
||||||
return sorted(d.name for d in base.iterdir() if d.is_dir())
|
|
||||||
|
|
||||||
def list_app_categories(self) -> dict[str, list[str]]:
|
|
||||||
apps_dir = self.repo_root / "apps"
|
|
||||||
if not apps_dir.is_dir():
|
|
||||||
return {}
|
|
||||||
categories = {}
|
|
||||||
for d in sorted(apps_dir.iterdir()):
|
|
||||||
if d.is_dir() and d.name != "install":
|
|
||||||
scripts = sorted(f.stem for f in d.glob("*.sh"))
|
|
||||||
if scripts:
|
|
||||||
categories[d.name] = scripts
|
|
||||||
return categories
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
||||||
|
|
||||||
from gui.services.pos_runner import PosRunner
|
|
||||||
|
|
||||||
runner = PosRunner()
|
|
||||||
|
|
||||||
templates = Environment(
|
|
||||||
loader=FileSystemLoader(Path(__file__).parent / "templates"),
|
|
||||||
autoescape=select_autoescape(["html", "xml"]),
|
|
||||||
)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main {
|
|
||||||
margin-top: 3.25rem;
|
|
||||||
min-height: calc(100vh - 3.25rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
background: #0a0a0a;
|
|
||||||
padding: 1.5rem 0;
|
|
||||||
border-right: 1px solid #1a1a1a;
|
|
||||||
min-height: calc(100vh - 3.25rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .menu-list a {
|
|
||||||
color: #b0b0b0;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .menu-list a:hover {
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .menu-list a.is-active {
|
|
||||||
background: #3273dc;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
background: #0d0d0d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-box {
|
|
||||||
background: #111;
|
|
||||||
color: #00e676;
|
|
||||||
border: 1px solid #222;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 1rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box {
|
|
||||||
background: #141414;
|
|
||||||
border: 1px solid #1e1e1e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
background: #141414;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
color: #b0b0b0;
|
|
||||||
border-color: #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table td {
|
|
||||||
border-color: #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background: #0a0a0a;
|
|
||||||
border-bottom: 1px solid #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-item {
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-item:hover {
|
|
||||||
background: #1a1a1a;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.sidebar {
|
|
||||||
display: none;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
.sidebar.is-active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.main-content {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Linux_post_install</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.3/dist/htmx.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar is-fixed-top" role="navigation">
|
|
||||||
<div class="navbar-brand">
|
|
||||||
<a class="navbar-item has-text-weight-bold" href="/dashboard">
|
|
||||||
Linux_post_install
|
|
||||||
</a>
|
|
||||||
<button class="navbar-burger" data-target="sidebar">
|
|
||||||
<span></span><span></span><span></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="columns is-gapless" id="main">
|
|
||||||
<aside class="column is-2 sidebar" id="sidebar">
|
|
||||||
<ul class="menu-list">
|
|
||||||
<li><a class="{{ 'is-active' if active == 'dashboard' else '' }}" href="/dashboard">Dashboard</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'install' else '' }}" href="/install">Install</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'compose' else '' }}" href="/compose">Docker Compose</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'network' else '' }}" href="/network">Network</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'vbox' else '' }}" href="/vbox">VBox</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'system' else '' }}" href="/system/firewall">Firewall</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'media' else '' }}" href="/media">Media</a></li>
|
|
||||||
<li><a class="{{ 'is-active' if active == 'ssh' else '' }}" href="/ssh">SSH</a></li>
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
<main class="column main-content">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<nav class="breadcrumb"><ul><li><a href="/compose">Compose</a></li><li class="is-active"><a href="#">Config</a></li></ul></nav>
|
|
||||||
<h1 class="title">Global Compose Config</h1>
|
|
||||||
<div class="box">
|
|
||||||
<p class="subtitle">File: ~/.config/linux_post_install/compose.env</p>
|
|
||||||
<pre class="log-box">{{ current }}</pre>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Set a Value</h2>
|
|
||||||
<form hx-post="/compose/config/set" hx-target="#config-result">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" name="key" placeholder="TS_AUTHKEY" required>
|
|
||||||
</div>
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="value" placeholder="value" required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary">Set</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="config-result"></div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Config Keys</h2>
|
|
||||||
<table class="table">
|
|
||||||
<thead><tr><th>Key</th><th>Required</th><th>Default</th><th>Purpose</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>TS_AUTHKEY</td><td>Yes</td><td>—</td><td>Tailscale auth key</td></tr>
|
|
||||||
<tr><td>TZ</td><td>No</td><td>Europe/Amsterdam</td><td>Timezone</td></tr>
|
|
||||||
<tr><td>DNS_SERVER</td><td>No</td><td>9.9.9.9</td><td>Custom DNS</td></tr>
|
|
||||||
<tr><td>SERVICES_BASE</td><td>No</td><td>/srv</td><td>Deployment directory</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<nav class="breadcrumb"><ul><li><a href="/compose">Compose</a></li><li class="is-active"><a href="#">{{ service }}</a></li></ul></nav>
|
|
||||||
<h1 class="title">{{ service }}</h1>
|
|
||||||
<div class="buttons">
|
|
||||||
<button class="button is-success" onclick="runAction('up')">Up</button>
|
|
||||||
<button class="button is-danger" onclick="runAction('down')">Down</button>
|
|
||||||
<button class="button is-warning" onclick="runAction('restart')">Restart</button>
|
|
||||||
<button class="button is-info" onclick="runAction('logs')">Logs</button>
|
|
||||||
</div>
|
|
||||||
<pre id="output" class="log-box"></pre>
|
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
function runAction(action) {
|
|
||||||
const el = document.getElementById('output');
|
|
||||||
el.textContent = '';
|
|
||||||
const evt = new EventSource(`/compose/{{ service }}/stream/${action}`);
|
|
||||||
evt.addEventListener('stdout', e => { el.textContent += e.data + '\n'; });
|
|
||||||
evt.addEventListener('stderr', e => { el.textContent += e.data + '\n'; });
|
|
||||||
evt.addEventListener('exit', e => { evt.close(); });
|
|
||||||
evt.addEventListener('error', e => { el.textContent += '\nERROR: ' + e.data + '\n'; });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">Docker Compose</h1>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Available Services ({{ services | length }})</h2>
|
|
||||||
<div class="list is-hoverable" style="max-height:500px;overflow-y:auto;">
|
|
||||||
{% for svc in services %}
|
|
||||||
<a class="list-item" href="/compose/{{ svc }}">{{ svc }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Deployed ({{ deployed | length }})</h2>
|
|
||||||
<div class="list is-hoverable">
|
|
||||||
{% for svc in deployed %}
|
|
||||||
<a class="list-item" href="/compose/{{ svc }}">{{ svc }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% if not deployed %}
|
|
||||||
<p class="has-text-grey">No services deployed yet.</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box mt-4">
|
|
||||||
<h2 class="subtitle">Global Config</h2>
|
|
||||||
<a class="button is-small" href="/compose/config">View / Edit Config</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">Dashboard</h1>
|
|
||||||
<div class="columns is-multiline">
|
|
||||||
<div class="column is-3">
|
|
||||||
<div class="box">
|
|
||||||
<p class="heading">Docker</p>
|
|
||||||
<p class="title">{{ 'Installed' if docker_ok else 'Not installed' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
|
||||||
<div class="box">
|
|
||||||
<p class="heading">Available Services</p>
|
|
||||||
<p class="title">{{ compose_count }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
|
||||||
<div class="box">
|
|
||||||
<p class="heading">Deployed Services</p>
|
|
||||||
<p class="title">{{ deployed_count }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-3">
|
|
||||||
<div class="box">
|
|
||||||
<p class="heading">Optional Apps</p>
|
|
||||||
<p class="title">{{ total_apps }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">Installation</h1>
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Select Phases</h2>
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox"><input type="checkbox" class="phase-checkbox" value="1" checked> Phase 1: System Packages</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox"><input type="checkbox" class="phase-checkbox" value="2" checked> Phase 2: Install Scripts</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox"><input type="checkbox" class="phase-checkbox" value="3" checked> Phase 3: Post-install Config</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox"><input type="checkbox" class="phase-checkbox" value="4" checked> Phase 4: ScaleTail Templates</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if app_categories %}
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Optional Apps</h2>
|
|
||||||
{% for category, apps in app_categories.items() %}
|
|
||||||
<div class="mb-3">
|
|
||||||
<h3 class="has-text-weight-bold">{{ category | capitalize }}</h3>
|
|
||||||
{% for app in apps %}
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox"><input type="checkbox" class="app-checkbox" value="{{ category }}/{{ app }}"> {{ app }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<button class="button is-primary" onclick="startInstall()">Start Installation</button>
|
|
||||||
<div id="output" class="mt-4" style="display:none;">
|
|
||||||
<pre id="log" class="log-box"></pre>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<script>
|
|
||||||
function startInstall() {
|
|
||||||
const phases = Array.from(document.querySelectorAll('.phase-checkbox:checked')).map(c => c.value);
|
|
||||||
const apps = Array.from(document.querySelectorAll('.app-checkbox:checked')).map(c => c.value);
|
|
||||||
if (phases.length === 0 && apps.length === 0) return;
|
|
||||||
const el = document.getElementById('output');
|
|
||||||
const log = document.getElementById('log');
|
|
||||||
el.style.display = 'block';
|
|
||||||
log.textContent = '';
|
|
||||||
const evt = new EventSource(`/install/stream?phases=${phases.join(',')}&apps=${apps.join(',')}`);
|
|
||||||
evt.addEventListener('phase_start', e => { log.textContent += '\n=== ' + e.data + ' ===\n'; });
|
|
||||||
evt.addEventListener('stdout', e => { log.textContent += e.data + '\n'; });
|
|
||||||
evt.addEventListener('stderr', e => { log.textContent += e.data + '\n'; });
|
|
||||||
evt.addEventListener('exit', e => { log.textContent += '\nDone (exit: ' + e.data + ')\n'; evt.close(); });
|
|
||||||
evt.addEventListener('error', e => { log.textContent += '\nERROR: ' + e.data + '\n'; });
|
|
||||||
log.scrollTop = log.scrollHeight;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">Media Download</h1>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">MP3 (Audio)</h2>
|
|
||||||
<form hx-post="/media/mp3/stream" hx-trigger="submit" hx-target="#mp3-output">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="url" placeholder="https://youtube.com/watch?v=..." required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary">Download MP3</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<pre id="mp3-output" class="log-box mt-2"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">MP4 (Video)</h2>
|
|
||||||
<form hx-post="/media/mp4/formats" hx-target="#formats-output" hx-trigger="submit">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="url" placeholder="https://youtube.com/watch?v=..." required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-info">List Formats</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<pre id="formats-output" class="log-box mt-2"></pre>
|
|
||||||
<form hx-post="/media/mp4/stream" hx-trigger="submit" hx-target="#mp4-output" class="mt-3">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="format_id" placeholder="format code from above">
|
|
||||||
</div>
|
|
||||||
<input type="hidden" name="url" id="mp4-url-input">
|
|
||||||
</div>
|
|
||||||
<button class="button is-primary">Download MP4</button>
|
|
||||||
</form>
|
|
||||||
<pre id="mp4-output" class="log-box mt-2"></pre>
|
|
||||||
<script>
|
|
||||||
document.querySelector('form[hx-post*="mp4/formats"]').addEventListener('htmx:afterRequest', function(evt) {
|
|
||||||
const url = evt.detail.elt.querySelector('[name=url]').value;
|
|
||||||
document.getElementById('mp4-url-input').value = url;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<p>Target: <strong>{{ target }}</strong></p>
|
|
||||||
<pre class="log-box">{{ output }}</pre>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">Network</h1>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">IP Info</h2>
|
|
||||||
<a class="button" href="/network/ip" hx-get="/network/ip" hx-target="#ip-result" hx-swap="innerHTML">Show IP Info</a>
|
|
||||||
<div id="ip-result"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Check Port</h2>
|
|
||||||
<form hx-post="/network/checkport" hx-target="#port-result">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="target" placeholder="192.168.1.1:80" required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary">Check</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="port-result"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Network Scan</h2>
|
|
||||||
<form hx-post="/network/scan/stream" hx-trigger="submit" hx-target="#scan-output">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="target" placeholder="192.168.1.0/24" required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary">Scan</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="checkbox"><input type="checkbox" name="full"> Full scan (OS detection, services)</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<pre id="scan-output" class="log-box mt-2"></pre>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<pre class="log-box">{{ output }}</pre>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">SSH Keys</h1>
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Load Keys into ssh-agent</h2>
|
|
||||||
<button class="button is-primary" hx-post="/ssh/load" hx-target="#ssh-result">Load Keys</button>
|
|
||||||
<div id="ssh-result" class="mt-2">
|
|
||||||
{% if output %}
|
|
||||||
<pre class="log-box">{{ output }}</pre>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">Firewall</h1>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Status</h2>
|
|
||||||
<pre class="log-box">{{ status }}</pre>
|
|
||||||
<div class="buttons mt-2">
|
|
||||||
<button class="button is-success" hx-post="/system/firewall/enable" hx-target="#fw-result">Enable</button>
|
|
||||||
<button class="button is-danger" hx-post="/system/firewall/disable" hx-target="#fw-result">Disable</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Add Rule</h2>
|
|
||||||
<form hx-post="/system/firewall/allow" hx-target="#fw-result">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="port" placeholder="80 or 3000:3100" required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<div class="select">
|
|
||||||
<select name="protocol">
|
|
||||||
<option value="tcp">tcp</option>
|
|
||||||
<option value="udp">udp</option>
|
|
||||||
<option value="">both</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-success">Allow</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form hx-post="/system/firewall/deny" hx-target="#fw-result" class="mt-2">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="port" placeholder="port number" required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<div class="select">
|
|
||||||
<select name="protocol">
|
|
||||||
<option value="tcp">tcp</option>
|
|
||||||
<option value="udp">udp</option>
|
|
||||||
<option value="">both</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-danger">Deny</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Delete Rule</h2>
|
|
||||||
<form hx-post="/system/firewall/delete" hx-target="#fw-result">
|
|
||||||
<div class="field has-addons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<input class="input" name="rule_num" placeholder="rule number (e.g. 3)" required>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-warning">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<button class="button is-small mt-2" hx-get="/system/firewall/status" hx-target="#fw-result">Show numbered rules</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="fw-result" class="mt-2"></div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<section class="section">
|
|
||||||
<h1 class="title">VBox — Disposable Docker VMs</h1>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Create</h2>
|
|
||||||
<form hx-post="/vbox/create" hx-target="#vbox-result">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Name</label>
|
|
||||||
<input class="input" name="name" placeholder="my-vm" required>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Image</label>
|
|
||||||
<input class="input" name="image" value="ubuntu:22.04">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Directory (optional)</label>
|
|
||||||
<input class="input" name="directory" placeholder="e.g. /home/user/project">
|
|
||||||
</div>
|
|
||||||
<button class="button is-primary">Create</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<div class="box">
|
|
||||||
<h2 class="subtitle">Containers</h2>
|
|
||||||
<pre class="log-box">{{ containers }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="vbox-result"></div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
+49
-1
@@ -12,9 +12,11 @@ if [ "$NO_COLOR" -eq 1 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
source "$(dirname "$0")/lib/common.sh"
|
source "$(dirname "$0")/lib/common.sh"
|
||||||
|
source "$(dirname "$0")/lib/flags.sh"
|
||||||
|
|
||||||
DRY_RUN=0
|
DRY_RUN=0
|
||||||
RUN_APPS=0
|
RUN_APPS=0
|
||||||
|
RUN_FEATURES=0
|
||||||
SKIP_PHASES=""
|
SKIP_PHASES=""
|
||||||
STEPS_SPEC=""
|
STEPS_SPEC=""
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ Bootstrap a fresh Debian/Ubuntu install.
|
|||||||
Options:
|
Options:
|
||||||
--apps Run interactive app picker after core install
|
--apps Run interactive app picker after core install
|
||||||
--full Core install + all optional apps (non-interactive)
|
--full Core install + all optional apps (non-interactive)
|
||||||
|
--feature Install features/ scripts (prompts before overwriting)
|
||||||
--dry-run Show what would be done without executing
|
--dry-run Show what would be done without executing
|
||||||
--skip <phase> Skip a phase (repeatable):
|
--skip <phase> Skip a phase (repeatable):
|
||||||
preinstall, scripts, postinstall, scalepoint, apps
|
preinstall, scripts, postinstall, scalepoint, apps
|
||||||
@@ -34,6 +37,11 @@ Options:
|
|||||||
(1=preinstall, 2=scripts, 3=postinstall, 4=scalepoint)
|
(1=preinstall, 2=scripts, 3=postinstall, 4=scalepoint)
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-h, --help Show this help message
|
-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 <app>... # uninstall specific apps
|
||||||
EOF
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
@@ -42,6 +50,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
--apps) RUN_APPS=1; shift ;;
|
--apps) RUN_APPS=1; shift ;;
|
||||||
--full) RUN_APPS=2; shift ;;
|
--full) RUN_APPS=2; shift ;;
|
||||||
|
--feature) RUN_FEATURES=1; shift ;;
|
||||||
--dry-run) DRY_RUN=1; shift ;;
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
--skip)
|
--skip)
|
||||||
[ -z "${2:-}" ] && err "Missing value for --skip"
|
[ -z "${2:-}" ] && err "Missing value for --skip"
|
||||||
@@ -103,7 +112,46 @@ if should_run 2 scripts; then
|
|||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
done
|
done
|
||||||
run sudo install -m 644 lib/common.sh /usr/local/bin/common.sh
|
run sudo install -m 644 lib/common.sh /usr/local/bin/common.sh
|
||||||
ok "$count scripts + lib -> /usr/local/bin"
|
run sudo install -m 644 lib/flags.sh /usr/local/bin/flags.sh
|
||||||
|
|
||||||
|
# ── Precompiled architecture binaries ─────────────────────
|
||||||
|
# Manually-compiled binaries (not available on the internet),
|
||||||
|
# copied straight into /usr/local/bin for the matching arch.
|
||||||
|
# Drop an arm64_bin/ folder later — it is picked up automatically.
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) prebuilt_dir="x64_bin" ;;
|
||||||
|
aarch64|arm64) prebuilt_dir="arm64_bin" ;;
|
||||||
|
*) prebuilt_dir="" ;;
|
||||||
|
esac
|
||||||
|
if [ -n "$prebuilt_dir" ] && [ -d "$prebuilt_dir" ]; then
|
||||||
|
for f in "$prebuilt_dir"/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
run sudo install -m 755 "$f" /usr/local/bin/
|
||||||
|
done
|
||||||
|
ok "$prebuilt_dir binaries -> /usr/local/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Optional features ──────────────────────────────────────
|
||||||
|
if [ "$RUN_FEATURES" -eq 1 ]; then
|
||||||
|
for f in features/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
name=$(basename "$f")
|
||||||
|
flag_name="${name%.sh}"
|
||||||
|
dest="/usr/local/bin/$name"
|
||||||
|
if [ -e "$dest" ]; then
|
||||||
|
if confirm "Overwrite existing $dest?" n; then
|
||||||
|
run sudo install -m 755 "$f" "$dest"
|
||||||
|
else
|
||||||
|
log "Keeping existing $dest"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
run sudo install -m 755 "$f" "$dest"
|
||||||
|
fi
|
||||||
|
flag_set "$flag_name"
|
||||||
|
done
|
||||||
|
ok "features installed"
|
||||||
|
fi
|
||||||
|
ok "$count scripts + libs -> /usr/local/bin"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Phase 3: postinstall ───────────────────────────────────────
|
# ── Phase 3: postinstall ───────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# ── Feature flag store ─────────────────────────────────────────
|
||||||
|
# System-wide flags: one file per flag in $FLAGS_DIR.
|
||||||
|
# presence = flag set, file content = optional value.
|
||||||
|
# Sourced by scripts; reads are plain file ops, writes use sudo.
|
||||||
|
# Override FLAGS_DIR via environment for testing.
|
||||||
|
FLAGS_DIR="${FLAGS_DIR:-/usr/local/share/linux_post_install/flags}"
|
||||||
|
|
||||||
|
# flag_set <name> [value] — mark a flag as set (green), optionally with a value
|
||||||
|
flag_set() {
|
||||||
|
local name="$1" value="${2:-}" tmp
|
||||||
|
run sudo install -d -m 755 "$FLAGS_DIR"
|
||||||
|
if [ -n "$value" ]; then
|
||||||
|
tmp=$(mktemp)
|
||||||
|
printf '%s' "$value" > "$tmp"
|
||||||
|
run sudo install -m 644 "$tmp" "$FLAGS_DIR/$name"
|
||||||
|
rm -f "$tmp"
|
||||||
|
else
|
||||||
|
run sudo touch "$FLAGS_DIR/$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# flag_clear <name> — remove a flag
|
||||||
|
flag_clear() {
|
||||||
|
run sudo rm -f "$FLAGS_DIR/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# flag_is_set <name> — 0 if flag is set, 1 otherwise
|
||||||
|
flag_is_set() {
|
||||||
|
[ -f "$FLAGS_DIR/$1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# flag_value <name> — print the stored value (empty when unset or valueless)
|
||||||
|
flag_value() {
|
||||||
|
local f="$FLAGS_DIR/$1"
|
||||||
|
[ -f "$f" ] && cat "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
# flag_list — print the names of all set flags, one per line
|
||||||
|
flag_list() {
|
||||||
|
[ -d "$FLAGS_DIR" ] || return 0
|
||||||
|
for f in "$FLAGS_DIR"/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
printf '%s\n' "$(basename "$f")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# flag_status <name> — print a human-readable status line
|
||||||
|
flag_status() {
|
||||||
|
local name="$1" v
|
||||||
|
if flag_is_set "$name"; then
|
||||||
|
v=$(flag_value "$name")
|
||||||
|
if [ -n "$v" ]; then
|
||||||
|
echo "set: $name=$v"
|
||||||
|
else
|
||||||
|
echo "set: $name"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "unset: $name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "$0")/lib/common.sh"
|
source "$(dirname "$0")/lib/common.sh"
|
||||||
|
source "$(dirname "$0")/lib/flags.sh"
|
||||||
|
|
||||||
log "Running post-install..."
|
log "Running post-install..."
|
||||||
|
|
||||||
@@ -79,6 +80,12 @@ if [ -d systemd ] && [ -n "$(ls -A systemd/*.service 2>/dev/null)" ]; then
|
|||||||
|
|
||||||
for svc in systemd/*.service; do
|
for svc in systemd/*.service; do
|
||||||
svc_name=$(basename "$svc")
|
svc_name=$(basename "$svc")
|
||||||
|
# autostart.service runs features/autostart.sh — enable only when
|
||||||
|
# the autostart feature flag is green (set by ./install.sh --feature)
|
||||||
|
if [ "$svc_name" = "autostart.service" ] && ! flag_is_set autostart; then
|
||||||
|
warn "autostart feature not installed — skipping autostart.service (run ./install.sh --feature)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
run sudo systemctl enable --now "$svc_name" 2>/dev/null || \
|
run sudo systemctl enable --now "$svc_name" 2>/dev/null || \
|
||||||
run sudo systemctl enable "$svc_name"
|
run sudo systemctl enable "$svc_name"
|
||||||
done
|
done
|
||||||
|
|||||||
+3
-1
@@ -26,12 +26,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
PACKAGES=(
|
PACKAGES=(
|
||||||
git curl wget vim nano tmux tree jq
|
git curl wget bc vim nano tmux tree jq
|
||||||
unzip zip rsync htop btop telnet
|
unzip zip rsync htop btop telnet
|
||||||
net-tools iputils-ping traceroute tcpdump nmap
|
net-tools iputils-ping traceroute tcpdump nmap
|
||||||
openssh-client openssh-server ufw fail2ban
|
openssh-client openssh-server ufw fail2ban
|
||||||
|
hostapd dnsmasq iptables iw
|
||||||
ca-certificates gnupg lsb-release
|
ca-certificates gnupg lsb-release
|
||||||
python3 python3-pip rclone
|
python3 python3-pip rclone
|
||||||
|
libqrencode4 libgtk-3-0
|
||||||
)
|
)
|
||||||
|
|
||||||
spawn "apt update" sudo apt update
|
spawn "apt update" sudo apt update
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# Regenerate code-derived doc sections between GEN markers.
|
||||||
|
#
|
||||||
|
# scripts/gen-docs.sh rewrite files in place
|
||||||
|
# scripts/gen-docs.sh --check verify only; exit 1 on any drift
|
||||||
|
#
|
||||||
|
# Sources of truth:
|
||||||
|
# - bin/pos-* filenames → category, subcommand
|
||||||
|
# - "# POS:" header line → one-line description
|
||||||
|
# - "# POS_FLAGS:" line → flag completion list (flag-style tools only)
|
||||||
|
|
||||||
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
mode="write"
|
||||||
|
[ "${1:-}" = "--check" ] && mode="check"
|
||||||
|
|
||||||
|
ctx="$root/DOC/AGENT_Context_Project.md"
|
||||||
|
comp="$root/completions/pos.bash"
|
||||||
|
|
||||||
|
# ── Collect tools: "cat|sub|desc|flags" ────────────────────────
|
||||||
|
tools=()
|
||||||
|
for f in "$root"/bin/pos-*; do
|
||||||
|
[ -x "$f" ] || continue
|
||||||
|
name="${f##*/pos-}"
|
||||||
|
cat="${name%%-*}"
|
||||||
|
sub="${name#*-}"
|
||||||
|
desc="$(sed -n '/^# POS: /{s/^# POS: //;p;q}' "$f")"
|
||||||
|
[ -n "$desc" ] || { echo "gen-docs: no '# POS:' header in $f" >&2; exit 1; }
|
||||||
|
desc="${desc#*— }"
|
||||||
|
flags="$(sed -n '/^# POS_FLAGS: /{s/^# POS_FLAGS: //;p;q}' "$f")"
|
||||||
|
tools+=("$cat|$sub|$desc|$flags")
|
||||||
|
done
|
||||||
|
mapfile -t tools < <(printf '%s\n' "${tools[@]}" | sort)
|
||||||
|
|
||||||
|
# ── Block generators (emit inner content only, no markers) ──────
|
||||||
|
gen_tree() {
|
||||||
|
local width=0 cat sub desc flags name t
|
||||||
|
for t in "${tools[@]}"; do
|
||||||
|
IFS='|' read -r cat sub desc flags <<<"$t"
|
||||||
|
name="pos-$cat-$sub"
|
||||||
|
[ ${#name} -gt "$width" ] && width=${#name}
|
||||||
|
done
|
||||||
|
for t in "${tools[@]}"; do
|
||||||
|
IFS='|' read -r cat sub desc flags <<<"$t"
|
||||||
|
name="pos-$cat-$sub"
|
||||||
|
printf '│ ├── %-*s# %s\n' "$((width + 1))" "$name" "$desc"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_dispatch() {
|
||||||
|
local cat sub desc flags t
|
||||||
|
for t in "${tools[@]}"; do
|
||||||
|
IFS='|' read -r cat sub desc flags <<<"$t"
|
||||||
|
printf '| %s | %s | `pos-%s-%s` | %s |\n' "$cat" "$sub" "$cat" "$sub" "$desc"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_selfcontained() {
|
||||||
|
local list=() f base out=""
|
||||||
|
for f in "$root"/bin/pos "$root"/bin/pos-*; do
|
||||||
|
[ -x "$f" ] || continue
|
||||||
|
base="$(basename "$f")"
|
||||||
|
grep -q 'common\.sh' "$f" || list+=("$base")
|
||||||
|
done
|
||||||
|
for b in "${list[@]}"; do
|
||||||
|
out+="\`$b\`, "
|
||||||
|
done
|
||||||
|
echo "${out%, }."
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_filetable() {
|
||||||
|
local cat sub desc flags name t
|
||||||
|
printf '| `bin/pos` | %s | CLI dispatcher with smart arg matching + logging + category help |\n' "$(wc -l < "$root/bin/pos")"
|
||||||
|
for t in "${tools[@]}"; do
|
||||||
|
IFS='|' read -r cat sub desc flags <<<"$t"
|
||||||
|
name="bin/pos-$cat-$sub"
|
||||||
|
printf '| `%s` | %s | %s |\n' "$name" "$(wc -l < "$root/$name")" "$desc"
|
||||||
|
done
|
||||||
|
printf '| `completions/pos.bash` | %s | Dynamic bash completion |\n' "$(wc -l < "$comp")"
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_posflags() {
|
||||||
|
local cat sub desc flags t
|
||||||
|
echo "declare -A _pos_flags"
|
||||||
|
for t in "${tools[@]}"; do
|
||||||
|
IFS='|' read -r cat sub desc flags <<<"$t"
|
||||||
|
[ -n "$flags" ] || continue
|
||||||
|
printf '_pos_flags[%s-%s]="%s"\n' "$cat" "$sub" "$flags"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Replace (write) or verify (check) one marker block ──────────
|
||||||
|
markers() {
|
||||||
|
# bash files use '#' comment markers, markdown uses HTML comments
|
||||||
|
case "$1" in
|
||||||
|
*.bash|*.sh) echo "# GEN:START $2" "# GEN:END $2" ;;
|
||||||
|
*) echo "<!-- GEN:START $2 -->" "<!-- GEN:END $2 -->" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
regen_block() {
|
||||||
|
local file="$1" name="$2"
|
||||||
|
local start end newfile tmp
|
||||||
|
read -r start end <<<"$(markers "$file" "$name")"
|
||||||
|
newfile="$(mktemp)"
|
||||||
|
"gen_$name" > "$newfile"
|
||||||
|
|
||||||
|
grep -qF "$start" "$file" || { echo "gen-docs: missing marker '$start' in $file" >&2; rm -f "$newfile"; exit 1; }
|
||||||
|
|
||||||
|
if [ "$mode" = "check" ]; then
|
||||||
|
local cur
|
||||||
|
cur="$(sed -n "/^$start$/,/^$end$/p" "$file" | sed '1d;$d')"
|
||||||
|
if [ "$cur" != "$(cat "$newfile")" ]; then
|
||||||
|
echo "gen-docs: DRIFT in $file ($name block)" >&2
|
||||||
|
diff <(printf '%s\n' "$cur") <(cat "$newfile") >&2 || true
|
||||||
|
rm -f "$newfile"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
awk -v start="$start" -v end="$end" -v nf="$newfile" '
|
||||||
|
$0==start {
|
||||||
|
print
|
||||||
|
while ((getline line < nf) > 0) print line
|
||||||
|
skip=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
skip && $0==end { skip=0; print; next }
|
||||||
|
skip { next }
|
||||||
|
{ print }
|
||||||
|
' "$file" > "$tmp"
|
||||||
|
mv "$tmp" "$file"
|
||||||
|
fi
|
||||||
|
rm -f "$newfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
regen_block "$ctx" tree
|
||||||
|
regen_block "$ctx" dispatch
|
||||||
|
regen_block "$ctx" selfcontained
|
||||||
|
regen_block "$ctx" filetable
|
||||||
|
regen_block "$comp" posflags
|
||||||
|
|
||||||
|
echo "gen-docs: $mode OK"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=POS Entertainment Runner
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/pos-entertainment --run
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Run POS Entertainment Hourly
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitActiveSec=1h
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# TEMPLATE — new optional app installer
|
||||||
|
#
|
||||||
|
# 1. Copy: cp templates/app.sh apps/<category>/<name>.sh
|
||||||
|
# Categories: browsers, development, media, networking,
|
||||||
|
# remote-access, system, utilities.
|
||||||
|
# 2. Fill in install_myapp() / uninstall_myapp() (rename to your app).
|
||||||
|
# 3. Docs: add a row to the catalog table in DOC/APPS.md.
|
||||||
|
#
|
||||||
|
# Auto-appears in the `apps/install.sh` picker — no registration.
|
||||||
|
# Run as: bash apps/install.sh <name> (install)
|
||||||
|
# bash apps/install.sh --uninstall <name>
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_myapp() {
|
||||||
|
command -v myapp &>/dev/null && { log "myapp already installed"; return 0; }
|
||||||
|
|
||||||
|
# Pick one method (see conventions below) and wrap it in spawn:
|
||||||
|
spawn "Installing myapp" sudo apt install -y myapp
|
||||||
|
|
||||||
|
log "Run myapp: 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
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# CONVENTIONS — pick the install method that fits:
|
||||||
|
#
|
||||||
|
# APT package sudo apt install -y myapp
|
||||||
|
# remove: sudo apt purge -y myapp + apt autoremove -y
|
||||||
|
#
|
||||||
|
# Repo-based app add the apt repo in install; in uninstall also
|
||||||
|
# remove the .list file and keyring:
|
||||||
|
# sudo rm -f /etc/apt/sources.list.d/myapp.list /usr/share/keyrings/...
|
||||||
|
#
|
||||||
|
# Official script spawn "Installing myapp" bash -c "curl -fsSL https://.../install.sh | sh"
|
||||||
|
# uninstall: remove the installed binary/files
|
||||||
|
#
|
||||||
|
# Flatpak sudo flatpak install -y flathub <app-id>
|
||||||
|
# remove: sudo flatpak uninstall -y <app-id>
|
||||||
|
#
|
||||||
|
# .deb download download to temp, sudo apt install -y ./file.deb
|
||||||
|
#
|
||||||
|
# File/AppImage install under /opt/<app>; in uninstall remove the
|
||||||
|
# files, symlinks, and desktop entries
|
||||||
|
#
|
||||||
|
# usermod for groups sudo usermod -aG <group> "$USER" + print a
|
||||||
|
# re-login reminder
|
||||||
|
#
|
||||||
|
# Every app MUST be idempotent (guard install AND uninstall) and MUST
|
||||||
|
# provide uninstall_myapp() + the uninstall case dispatch above.
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# TEMPLATE — new feature script
|
||||||
|
#
|
||||||
|
# 1. Copy: cp templates/feature.sh features/<name>.sh
|
||||||
|
# 2. Docs: DOC/AGENT_Context_Project.md file table line counts.
|
||||||
|
#
|
||||||
|
# Installed on demand with: ./install.sh --feature
|
||||||
|
# → copied to /usr/local/bin/<name>.sh (chmod 755)
|
||||||
|
# → flag "<name>" is set (basename of the file, minus .sh)
|
||||||
|
# Features are user-customizable — install.sh never overwrites an
|
||||||
|
# existing /usr/local/bin copy without asking.
|
||||||
|
#
|
||||||
|
# If a systemd service depends on this feature, gate it in
|
||||||
|
# postinstall.sh's systemd loop:
|
||||||
|
# if [ "$svc_name" = "<name>.service" ] && ! flag_is_set <name>; then
|
||||||
|
# warn "<name> feature not installed — skipping <name>.service"
|
||||||
|
# continue
|
||||||
|
# fi
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Robust flags.sh load — works from the repo checkout AND from
|
||||||
|
# /usr/local/bin after install.sh (which copies lib/flags.sh there).
|
||||||
|
source "$(dirname "$0")/../lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||||
|
|
||||||
|
# Self-name → matches the flag install.sh sets for this feature.
|
||||||
|
FEATURE_NAME="$(basename "$0")"
|
||||||
|
FEATURE_NAME="${FEATURE_NAME%.sh}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ${FEATURE_NAME}.sh [options]
|
||||||
|
|
||||||
|
<describe what this feature does>
|
||||||
|
|
||||||
|
Installed via: ./install.sh --feature
|
||||||
|
Flag: ${FEATURE_NAME}
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── script logic ────────────────────────────────────────────────
|
||||||
|
# Feature scripts may run repeatedly (e.g. at every boot via a
|
||||||
|
# systemd service), so keep them idempotent.
|
||||||
|
#
|
||||||
|
# flag_is_set "$FEATURE_NAME" || exit 0 # bail when not installed
|
||||||
|
# v=$(flag_value "$FEATURE_NAME") # read an optional value
|
||||||
|
# flag_clear "$FEATURE_NAME" # uninstall behavior
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# TEMPLATE — new `pos` CLI tool
|
||||||
|
#
|
||||||
|
# 1. Copy: cp templates/pos-tool.sh bin/pos-<category>-<command>
|
||||||
|
# 2. Exec bit: chmod +x bin/pos-<category>-<command>
|
||||||
|
# 3. Register: add the command to usage() CATEGORIES/EXAMPLES in bin/pos
|
||||||
|
# If it reads stdin (password/selection prompts), also add it to
|
||||||
|
# INTERACTIVE_CMDS in bin/pos or its prompt breaks under the log tee.
|
||||||
|
# 4. Docs: DOC/POS.md section table + detail block,
|
||||||
|
# DOC/AGENT_Context_Project.md (bin tree, dispatch table,
|
||||||
|
# self-contained list, file line-count table), root README.md only
|
||||||
|
# when the category list changes.
|
||||||
|
# 5. Deps: add packages to PACKAGES in preinstall.sh if needed.
|
||||||
|
#
|
||||||
|
# Invoked as: pos <category> <command> [args]
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Robust common.sh load — works from the repo checkout AND from
|
||||||
|
# /usr/local/bin after install.sh (which copies lib/common.sh there).
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
# Optional runtime config (see DEV.md "Config files"):
|
||||||
|
# CONFIG_FILE="$HOME/.config/linux_post_install/<tool>.env"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos <category> <command> [args]
|
||||||
|
|
||||||
|
<describe what this command does>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos <category> <command> arg1
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── script logic ────────────────────────────────────────────────
|
||||||
|
# Use helpers from common.sh: log / warn / err / ok / section /
|
||||||
|
# step / run (respects --dry-run) / spawn / confirm.
|
||||||
|
#
|
||||||
|
# command -v <dep> &>/dev/null || err "<dep> not found"
|
||||||
|
# run sudo <command> # dry-run aware
|
||||||
|
# log "done" # green [+] message
|
||||||
|
#
|
||||||
|
# Exit 0 on success, err() exits 1 on failure.
|
||||||
Executable
+2044
File diff suppressed because it is too large
Load Diff
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Start wihotspot-gui
|
||||||
|
/usr/local/bin/wihotspot-gui
|
||||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user