Compare commits

...

17 Commits

Author SHA1 Message Date
Your Name 4e1c06a63e add placeholder entertaiment 2026-08-05 07:50:08 -04:00
Your Name 5244c21ba3 add weather api 2026-08-05 07:05:53 -04:00
Your Name e62626b55c ai need to continue ... 2026-08-05 05:47:45 -04:00
Your Name 494eae2bdf feat: add pos usb server (USB Redirector control); fix doc/code drift
- new bin/pos-usb-server: flag-style control of usbsrv (--ls, --share,
  --unshare, --auto-share, --callback, --close-callback, --auto-connect,
  --disconnect, --nickname, --timeout, --port, --info, --version);
  interactive prompts fall back to args; command -v usbsrv guard
- wire into bin/pos: usb category, example, INTERACTIVE_CMDS
- completions: usb-server flag completion
- docs: POS.md usb section, AGENT_Context tree/dispatch/table, README
- fix: add pos-network-hotspot to no-common.sh list (was omitted)
- fix: stale pos-docker-compose line count 317 -> 363
- fix: DEV.md/AGENT_Context note non-apt deps (command -v guard, not PACKAGES)
- fix: document INTERACTIVE_CMDS all-or-nothing per-script trade-off
- fix: usage() shows docker health (was missing from cheat-sheet)
2026-08-05 04:38:03 -04:00
Your Name 6d3482939e docs: note auto-discovery of category help in new-tool checklists 2026-08-05 04:17:02 -04:00
Your Name fe751bbc0c feat: add category help to pos dispatcher (pos <category> --help) 2026-08-05 04:09:19 -04:00
Your Name d591e71363 feat: show public IP location in pos network ip 2026-08-05 03:57:21 -04:00
Your Name 93f3044c2a docs: add AGENTS.md with lazy-loaded DOC references for agent guidance 2026-08-05 03:51:48 -04:00
Your Name 49fb248454 refactor: merge vbox into docker as pos docker vbox 2026-08-05 03:30:51 -04:00
Your Name 0c9d09c331 docs: add dev templates + clarify conventions; fix pos help for multi-word commands 2026-08-05 03:03:56 -04:00
Your Name 070524bafb feat: add tsui app installer and pos communication telegram sender 2026-08-05 02:42:35 -04:00
ciya e7c8c176ed feat: add pos system backup — encrypted folder snapshots with --service picker 2026-08-03 08:58:38 -04:00
Your Name db7bf4de86 chore: ignore session files and drop stale SSH key 2026-08-03 03:50:20 -04:00
Your Name 60718b360e feat: integrate precompiled hotspot binaries from x64_bin/ via pos network hotspot 2026-08-03 03:48:33 -04:00
Your Name 54592a5839 docs: reorganize into DOC/ and simplify README
- Move all docs (DEV, AGENT_Context, algorithm) into DOC/ via git mv
- Rewrite root README as short intro + quick start + links into DOC/
- Add per-area reference docs: SCRIPTS (core scripts + libs + features),
  POS (pos CLI + compose config), APPS (picker + catalog), SYSTEMD (units)
- Add DOC/README.md index; fix all cross-references
2026-08-03 02:50:38 -04:00
Your Name 2a2b4d81d1 feat: add feature-flag subsystem with on-demand features install
- Move autostart.sh from bin/ to features/ so re-installs never reset it
- Add lib/flags.sh (flag_set/clear/is_set/value/list/status) + flag-reader/set/clear CLIs
- install.sh --feature copies features/* to /usr/local/bin with overwrite prompt, sets flags
- postinstall.sh enables autostart.service only when the autostart flag is set
- Document features & flags in README, DEV.md, AGENT_Context_Project.md
2026-08-03 02:40:51 -04:00
Your Name cf7ba2d58a add x64 bins and arm64 bin 2026-08-03 02:08:58 -04:00
68 changed files with 4563 additions and 313 deletions
+2
View File
@@ -19,3 +19,5 @@ Thumbs.db
# Docker Compose — user's active stacks and secrets
compose/custom/
compose/config.env
session
+17
View File
@@ -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
├── 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/
│ ├── pos # Main dispatcher — smart arg matching to pos-* scripts
│ ├── pos-network-ip # Show interfaces, routes, public IP
│ ├── pos-network-checkport # TCP port checker
│ ├── pos-network-scan # Parallel ping sweep of CIDR subnet
│ ├── pos-docker-ps # Enhanced docker ps (health, IPs, ports, uptime)
│ ├── pos-docker-health # Quick one-glance health dashboard
│ ├── pos-docker-compose # Docker Compose service manager (largest script, 317 lines)
│ ├── pos-media-mp3 # Audio downloader (yt-dlp → MP3)
│ ├── pos-media-mp4 # Video downloader (yt-dlp → MP4, interactive format select)
│ ├── pos-system-firewall # Interactive UFW manager (menu-driven, 284 lines)
│ ├── pos-ssh-load-keys # Load SSH keys into ssh-agent
│ ├── pos-vbox # Disposable Docker-based "VMs"
│ ├── autostart.sh # Boot-time script (via systemd)
<!-- GEN:START tree -->
│ ├── pos-docker-compose # Docker Compose service manager (ls/up/down/restart/logs/update/config)
│ ├── pos-docker-health # One-glance container health dashboard (exits 1 if unhealthy)
│ ├── pos-docker-ps # Enhanced container overview (health, IPs, ports, uptime)
│ ├── pos-docker-vbox # Disposable Docker-based VMs (create/enter/start/stop/rm/ls)
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
│ ├── pos-media-mp4 # Download video as MP4 (interactive format select)
│ ├── pos-network-checkport # Check TCP port connectivity
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
│ ├── pos-network-ip # Show interfaces, routes, public IP + location
│ ├── pos-network-scan # Parallel ping sweep of CIDR
│ ├── 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)
│ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos
│ └── 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)
│ ├── install.sh # Interactive picker / orchestrator
│ ├── browsers/
@@ -78,6 +100,9 @@ Linux_post_install/
├── completions/
│ └── pos.bash # Bash tab-completion for the pos CLI
├── config/
│ └── authorized_keys # SSH public keys (gitignored)
├── compose/
│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)
@@ -85,8 +110,18 @@ Linux_post_install/
│ ├── autostart.service # Runs autostart.sh on boot
│ └── ssh-agent.service # System-wide SSH agent socket
├── README.md # User-facing documentation
├── DEV.md # Developer guide
├── README.md # User-facing intro + quick start (links into DOC/)
├── 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
└── .gitmodules # Submodule: compose/scale-tail → ScaleTail
```
@@ -96,20 +131,24 @@ Linux_post_install/
## 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)
│ └─ apt update + installs 25+ packages + yt-dlp + fail2ban
├─ Phase 2: install.sh (requires root)
│ └─ 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)
│ └─ Configures fail2ban (SSH jail: 5 retries, 1h ban)
│ └─ PATH export in ~/.bashrc
│ └─ Bash completion for pos CLI
│ └─ Copies systemd/*.service → /etc/systemd/system/, enables them
│ (autostart.service only when the `autostart` flag is set)
├─ Phase 4: ScaleTail clone
│ └─ 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 |
| `--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 |
| `--skip <phase>` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint`, `apps` |
| `--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 |
|----------|---------|--------|-------------|
| 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 | 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 |
| docker | ps | `pos-docker-ps` | Enhanced container overview |
| docker | health | `pos-docker-health` | Quick health dashboard (exits 1 if unhealthy) |
| 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 |
| ssh | load-keys | `pos-ssh-load-keys` | Load all SSH keys into the agent |
| system | backup | `pos-system-backup` | Encrypted (AES-256) folder snapshots (tar + gpg) |
| system | firewall | `pos-system-firewall` | Interactive UFW management |
| ssh | load-keys | `pos-ssh-load-keys` | Load SSH keys into agent |
| vbox | create | `pos-vbox create` | Create disposable VM (asks "Enter now?") |
| 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 |
| usb | server | `pos-usb-server` | USB Redirector server control (--ls, --share; prompts when args omitted) |
<!-- GEN:END dispatch -->
### 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`.
### 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
- **`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"
```
**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)
- Three modes: interactive (default), `--all`, or specific app names as arguments
- `--uninstall` switches to uninstall mode (same selection, invokes app scripts with `uninstall` argument)
- Interactive TUI groups apps by category with section headers
- Each app script is standalone, idempotent, sources `lib/common.sh`
- Every app script defines `install_<name>()` **and** `uninstall_<name>()`, dispatched via `case "${1:-}" in uninstall) ...`
### Installation Methods
@@ -283,7 +330,7 @@ ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar patter
### 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
---
@@ -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
- `~/.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
@@ -348,18 +404,28 @@ All `.service` files in `systemd/` are automatically copied to `/etc/systemd/sys
## 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
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
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
1. Create `bin/pos-<category>-<command>` following conventions
2. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)
3. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets)
4. Update `README.md`
5. Test: `bash -n bin/your-tool && shellcheck bin/your-tool`
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. Register in `bin/pos` `usage()` CATEGORIES/EXAMPLES; add to `INTERACTIVE_CMDS` in `bin/pos` if it reads stdin
3. Add system deps to `PACKAGES` array in `preinstall.sh` (if needed)
4. Add config logic to `postinstall.sh` (if needed, with `.gitignore` for secrets); runtime tool config → `~/.config/linux_post_install/<tool>.env` (600)
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
@@ -391,18 +457,32 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| File | Lines | Purpose |
|------|-------|---------|
| `install.sh` | ~120 | Main orchestrator — 4 phases with CLI flags |
| `preinstall.sh` | ~52 | System packages + yt-dlp + fail2ban |
| `postinstall.sh` | ~65 | fail2ban config, PATH, bash completion, systemd |
| `install.sh` | 192 | Main orchestrator — 4 phases with CLI flags, `--feature`, prebuilt arch bins |
| `preinstall.sh` | 54 | System packages + hotspot deps + yt-dlp + fail2ban |
| `postinstall.sh` | 97 | fail2ban config, PATH, bash completion, systemd (flag-gated) |
| `lib/common.sh` | 121 | Shared library |
| `bin/pos` | ~130 | CLI dispatcher with smart arg matching + logging |
| `bin/pos-docker-compose` | 317 | Largest script — full compose management |
| `bin/pos-system-firewall` | 284 | Interactive UFW manager |
| `bin/pos-docker-ps` | 127 | Enhanced container overview |
| `bin/pos-docker-health` | ~90 | Quick health dashboard |
| `bin/pos-vbox` | ~160 | Docker-based disposable VMs (label-filtered, auto-enter prompt) |
| `completions/pos.bash` | 118 | Dynamic bash completion |
| `apps/install.sh` | 99 | App picker/orchestrator |
| `lib/flags.sh` | 60 | Feature flag store (set/clear/is_set/value/list/status) |
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
| `bin/flag-clear` | 21 | Unset a flag |
| `features/autostart.sh` | 14 | Boot-time feature (moved from `bin/`, flag-gated service) |
| `bin/pos-communication-telegram` | 140 | Send Telegram messages via Bot API (--send, test, config set) |
| `bin/pos-docker-compose` | 364 | Docker Compose service manager (ls/up/down/restart/logs/update/config) |
| `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 |
|------|---------------|
| 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 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` |
| Change PATH or bash config | Edit `postinstall.sh` |
| Modify fail2ban config | Edit jail.local section in `postinstall.sh` |
| Add bash completion | Edit `completions/pos.bash` |
| Modify Docker Compose logic | Edit `bin/pos-docker-compose` |
| 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 pos logging | Edit log setup in `bin/pos` |
| 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` |
+88
View File
@@ -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
View File
@@ -49,6 +49,13 @@ pos docker compose up jellyfin
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`)
Sourced by most scripts. Key functions:
@@ -70,6 +77,8 @@ Sourced by most scripts. Key functions:
## 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
```bash
@@ -105,8 +114,17 @@ esac
warn() { echo "[!] $*"; }
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`:
@@ -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.
### 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
chmod +x bin/your-tool
bash -n bin/your-tool
shellcheck bin/your-tool
./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
Start from the template: `cp templates/app.sh apps/<category>/<name>.sh`.
### 1. Create the installer
```bash
@@ -153,20 +183,34 @@ install_myapp() {
spawn "Installing myapp" sudo apt install -y myapp
}
install_myapp
uninstall_myapp() {
command -v myapp &>/dev/null || { log "myapp not installed"; return 0; }
spawn "Removing myapp" sudo apt purge -y myapp
spawn "Cleaning up dependencies" sudo apt autoremove -y
}
case "${1:-}" in
uninstall) uninstall_myapp ;;
*) install_myapp ;;
esac
```
Place it in `apps/<category>/<name>.sh`. It auto-appears in the picker — no registration needed.
**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
- Idempotent: check `command -v` before installing
- APT packages → `sudo apt install -y` inside `spawn`
- Idempotent: check `command -v` (or `flatpak list` / file existence) before installing **and** uninstalling
- Every app **must** provide an `uninstall_<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`
- 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`
- File/AppImage installs → remove the installed files, symlinks, and desktop entries in uninstall
- `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/`
2. Understand its contract (args, output, exit codes)
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
---
@@ -216,8 +260,9 @@ run sudo apt install -y git
### 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
- Mask secrets in `config` output (see `pos-communication-telegram`'s `mask_token`)
- Validate input before shell commands
- Use `sudo` only where needed
@@ -226,10 +271,51 @@ run sudo apt install -y git
- CLI tools: `bin/pos-<category>-<command>`
- Legacy wrappers: `bin/wr-*`
- App installers: `apps/<category>/<name>.sh`
- Features: `features/<name>.sh`
- 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
Create `systemd/<name>.service``postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically.
+234
View File
@@ -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` |
+20
View File
@@ -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
View File
@@ -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.
+92
View File
@@ -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/`).
+1 -1
View File
@@ -325,7 +325,7 @@ pos docker compose up jellyfin
---
## 6. `pos-vbox` Lifecycle
## 6. `pos docker vbox` Lifecycle
```
create lab1
+28 -169
View File
@@ -2,22 +2,21 @@
> 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:**
- **25+ system packages** installed automatically (git, curl, tmux, ufw, fail2ban, etc.)
- **Unified `pos` CLI** — one command for network, Docker, media, system, and SSH tasks
- **119+ self-hosted services** via ScaleTail + Tailscale (Jellyfin, Home Assistant, etc.)
- **15 optional desktop apps** (VS Code, Docker Desktop, Brave, OBS, etc.) — pick what you want
- **systemd services** for SSH agent and boot-time automation
- **Everything in `/usr/local/bin/`** — you can delete the repo after install
---
- 25+ system packages installed automatically
- The `pos` CLI: network, Docker (compose + vbox VMs), media, system, SSH, USB, and communication (Telegram) tools
- Wi-Fi hotspot tools (`create_ap`, `wihotspot-gui`) via `pos network hotspot`
- 15 optional desktop apps (VS Code, Brave, OBS, Tailscale, …) — pick what you want
- 119+ self-hosted services with Tailscale access (Jellyfin, Home Assistant, …)
- systemd services for SSH agent and boot-time automation
- Everything lands in `/usr/local/bin/` — you can delete the repo after install
## 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
cd Linux_post_install
./install.sh # core: packages + CLI + services + ScaleTail
./install.sh --apps # core + interactive app picker
./install.sh --full # core + all apps (non-interactive)
./install.sh --feature # also install features/ scripts (asks before overwriting)
./install.sh --apps # also install optional desktop apps (interactive)
```
**Flags:**
| Flag | What it does |
|------|-------------|
| `--apps` | Run interactive app picker after core install |
| `--full` | Core install + all apps (no prompts) |
| `--dry-run` | Preview without executing anything |
| `--skip <phase>` | Skip a phase (repeatable): `preinstall`, `scripts`, `postinstall`, `scalepoint` |
| `--steps <spec>` | Run specific phases only, e.g. `--steps 1,3` or `--steps 1-3` |
| `--feature` | Install `features/` scripts to `/usr/local/bin/`, sets their flags |
| `--apps` | Interactive app picker after core install |
| `--full` | Core install + all apps (non-interactive) |
| `--dry-run` | Preview without executing |
| `--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 |
---
## Documentation
## What Gets Installed
| Phase | Script | What happens |
|-------|--------|-------------|
| 1 | `preinstall.sh` | `apt update` + 25+ packages + yt-dlp + fail2ban |
| 2 | `install.sh` | Copies all `bin/` tools to `/usr/local/bin/` |
| 3 | `postinstall.sh` | Configures fail2ban, SSH agent, PATH, bash completion, systemd services |
| 4 | ScaleTail clone | Downloads 119+ Docker Compose templates with Tailscale sidecar |
| 5 (opt) | `apps/install.sh` | Installs desktop apps you select |
---
## 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.
| Topic | Where |
|-------|-------|
| Docs index | [DOC/README.md](DOC/README.md) |
| Core scripts (installer, libs, features) — how they work + config | [DOC/SCRIPTS.md](DOC/SCRIPTS.md) |
| `pos` CLI reference (all commands, compose config, wrappers) | [DOC/POS.md](DOC/POS.md) |
| Optional apps (picker + full catalog) | [DOC/APPS.md](DOC/APPS.md) |
| Systemd services & bash completion | [DOC/SYSTEMD.md](DOC/SYSTEMD.md) |
| Developer guide (add tools/apps/features) | [DOC/DEV.md](DOC/DEV.md) |
| Algorithm diagrams | [DOC/algorithm.md](DOC/algorithm.md) |
+11 -1
View File
@@ -15,4 +15,14 @@ install_brave() {
spawn "Installing brave-browser" sudo apt install -y brave-browser
}
install_brave
uninstall_brave() {
command -v brave-browser &>/dev/null || { log "brave not installed"; return 0; }
spawn "Removing brave-browser" sudo apt purge -y brave-browser
spawn "Cleaning up dependencies" sudo apt autoremove -y
spawn "Removing brave apt repo" sudo rm -f /etc/apt/sources.list.d/brave-browser-release.list /usr/share/keyrings/brave-browser-archive-keyring.gpg
}
case "${1:-}" in
uninstall) uninstall_brave ;;
*) install_brave ;;
esac
+10 -1
View File
@@ -13,4 +13,13 @@ install_opencode() {
log "Add to PATH: export PATH=\"\$HOME/.opencode/bin:\$PATH\""
}
install_opencode
uninstall_opencode() {
[ -d "$HOME/.opencode" ] || { log "opencode not installed"; return 0; }
spawn "Removing opencode" rm -rf "$HOME/.opencode"
warn "Config/data remains in ~/.config/opencode — remove manually if desired"
}
case "${1:-}" in
uninstall) uninstall_opencode ;;
*) install_opencode ;;
esac
+11 -1
View File
@@ -15,4 +15,14 @@ install_vscode() {
spawn "Installing code" sudo apt install -y code
}
install_vscode
uninstall_vscode() {
command -v code &>/dev/null || { log "vscode not installed"; return 0; }
spawn "Removing code" sudo apt purge -y code
spawn "Cleaning up dependencies" sudo apt autoremove -y
spawn "Removing vscode apt repo" sudo rm -f /etc/apt/sources.list.d/vscode.list /usr/share/keyrings/packages.microsoft.gpg
}
case "${1:-}" in
uninstall) uninstall_vscode ;;
*) install_vscode ;;
esac
+47 -19
View File
@@ -6,26 +6,32 @@ usage() {
cat <<EOF
Usage: bash apps/install.sh [OPTIONS] [app ...]
Install optional desktop applications.
Install or uninstall optional desktop applications.
Options:
--all Install all available apps without prompting
-h, --help Show this help message
--all Apply action to all available apps without prompting
--uninstall Uninstall the selected apps instead of installing
-h, --help Show this help message
Examples:
bash apps/install.sh # interactive selection
bash apps/install.sh --all # install everything
bash apps/install.sh brave vscode # install specific apps
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 specific apps
EOF
exit 0
}
ALL=0
UNINSTALL=0
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all) ALL=1; shift ;;
--uninstall) UNINSTALL=1; shift ;;
-h|--help) usage ;;
-*) err "Unknown option: $1" ;;
*) POSITIONAL+=("$1"); shift ;;
@@ -62,11 +68,11 @@ for cat_dir in "$APPS_DIR"/*/; do
[ "$has_apps" -eq 0 ] && continue
CATEGORIES+=("$cat_name")
CAT_APPS["$cat_name"]=()
CAT_APPS["$cat_name"]=""
for f in "$cat_dir"*.sh; do
[ -f "$f" ] || continue
app_name=$(basename "$f" .sh)
CAT_APPS["$cat_name"]+=("$app_name")
CAT_APPS["$cat_name"]+=" $app_name"
done
done
@@ -74,7 +80,7 @@ done
find_app_category() {
local app="$1"
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; }
done
done
@@ -96,7 +102,7 @@ if [ "${#POSITIONAL[@]}" -gt 0 ]; then
# ── Mode 2: --all ─────────────────────────────────────────────
elif [ "$ALL" -eq 1 ]; then
for cat in "${CATEGORIES[@]}"; do
for app in "${CAT_APPS[$cat]}"; do
for app in ${CAT_APPS[$cat]}; do
SELECTED+=("$app")
done
done
@@ -104,14 +110,22 @@ elif [ "$ALL" -eq 1 ]; then
# ── Mode 3: interactive TUI ───────────────────────────────────
else
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
for cat in "${CATEGORIES[@]}"; do
display="${CAT_NAMES[$cat]:-$cat}"
echo " $display"
for app in "${CAT_APPS[$cat]}"; do
read -rp " Install ${app}? [y/N]: " yn
for app in ${CAT_APPS[$cat]}; do
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
SELECTED+=("$app")
fi
@@ -120,11 +134,15 @@ else
done
fi
# ── Install selected apps ──────────────────────────────────────
# ── Apply action to selected apps ─────────────────────────────
[ "${#SELECTED[@]}" -eq 0 ] && { warn "No apps selected"; exit 0; }
echo
section "Installing ${SELECTED[*]}"
if [ "$UNINSTALL" -eq 1 ]; then
section "Uninstalling ${SELECTED[*]}"
else
section "Installing ${SELECTED[*]}"
fi
timer_start
count=1
@@ -132,12 +150,22 @@ total=${#SELECTED[@]}
for app in "${SELECTED[@]}"; do
cat=$(find_app_category "$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))
echo
done
echo
echo "${GREEN}════════════════════════════════════════════${RESET}"
echo "${GREEN} Apps installed ($(timer_stop))${RESET}"
echo "${GREEN}════════════════════════════════════════════${RESET}"
if [ "$UNINSTALL" -eq 1 ]; then
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
View File
@@ -7,4 +7,13 @@ install_obs() {
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
View File
@@ -50,4 +50,13 @@ EOF
log "scrcpy $version installed (adb included in the bundle)"
}
install_scrcpy
uninstall_scrcpy() {
command -v scrcpy &>/dev/null || { log "scrcpy not installed"; return 0; }
spawn "Removing scrcpy files" sudo rm -rf /usr/local/lib/scrcpy-* /usr/local/bin/scrcpy /usr/share/applications/scrcpy.desktop
log "scrcpy removed (bundled adb was removed with it)"
}
case "${1:-}" in
uninstall) uninstall_scrcpy ;;
*) install_scrcpy ;;
esac
+10 -1
View File
@@ -7,4 +7,13 @@ install_vlc() {
spawn "Installing vlc" sudo apt install -y vlc
}
install_vlc
uninstall_vlc() {
command -v vlc &>/dev/null || { log "vlc not installed"; return 0; }
spawn "Removing vlc" sudo apt purge -y vlc
spawn "Cleaning up dependencies" sudo apt autoremove -y
}
case "${1:-}" in
uninstall) uninstall_vlc ;;
*) install_vlc ;;
esac
+11 -1
View File
@@ -11,4 +11,14 @@ install_netbird() {
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 -1
View File
@@ -11,4 +11,14 @@ install_tailscale() {
log "Start tailscale: sudo tailscale up"
}
install_tailscale
uninstall_tailscale() {
command -v tailscale &>/dev/null || { log "tailscale not installed"; return 0; }
spawn "Removing tailscale" sudo apt purge -y tailscale
spawn "Cleaning up dependencies" sudo apt autoremove -y
spawn "Removing tailscale apt repo" sudo rm -f /etc/apt/sources.list.d/tailscale.list /usr/share/keyrings/tailscale-archive-keyring.gpg
}
case "${1:-}" in
uninstall) uninstall_tailscale ;;
*) install_tailscale ;;
esac
+11 -1
View File
@@ -11,4 +11,14 @@ install_zerotier() {
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
+10 -1
View File
@@ -17,4 +17,13 @@ install_termius() {
log "Termius installed — launch with 'termius'"
}
install_termius
uninstall_termius() {
command -v termius &>/dev/null || { log "termius not installed"; return 0; }
spawn "Removing termius" sudo apt purge -y termius
spawn "Cleaning up dependencies" sudo apt autoremove -y
}
case "${1:-}" in
uninstall) uninstall_termius ;;
*) install_termius ;;
esac
+10 -1
View File
@@ -7,4 +7,13 @@ install_vnc_viewer() {
spawn "Installing tigervnc-viewer" sudo apt install -y tigervnc-viewer
}
install_vnc_viewer
uninstall_vnc_viewer() {
command -v vncviewer &>/dev/null || { log "tigervnc-viewer not installed"; return 0; }
spawn "Removing tigervnc-viewer" sudo apt purge -y tigervnc-viewer
spawn "Cleaning up dependencies" sudo apt autoremove -y
}
case "${1:-}" in
uninstall) uninstall_vnc_viewer ;;
*) install_vnc_viewer ;;
esac
+20 -1
View File
@@ -12,4 +12,23 @@ install_docker() {
warn "Log out and back in for docker group to take effect"
}
install_docker
uninstall_docker() {
command -v docker &>/dev/null || { log "docker not installed"; return 0; }
local pkgs=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-compose-v2)
local installed=()
for p in "${pkgs[@]}"; do
dpkg -s "$p" &>/dev/null && installed+=("$p")
done
if [ "${#installed[@]}" -gt 0 ]; then
spawn "Removing docker engine" sudo apt purge -y "${installed[@]}"
spawn "Cleaning up dependencies" sudo apt autoremove -y
fi
spawn "Removing docker apt repo" sudo rm -f /etc/apt/sources.list.d/docker.list /etc/apt/keyrings/docker.asc /etc/apt/keyrings/docker.gpg
warn "Docker data remains in /var/lib/docker — remove manually if desired"
}
case "${1:-}" in
uninstall) uninstall_docker ;;
*) install_docker ;;
esac
+13 -1
View File
@@ -15,4 +15,16 @@ install_qemu() {
warn "Log out and back in for libvirt/kvm groups to take effect"
}
install_qemu
uninstall_qemu() {
command -v qemu-system-x86_64 &>/dev/null || { log "qemu not installed"; return 0; }
spawn "Removing qemu and libvirt" sudo apt purge -y \
qemu-system qemu-utils qemu-kvm \
libvirt-daemon-system libvirt-clients \
bridge-utils virt-manager
spawn "Cleaning up dependencies" sudo apt autoremove -y
}
case "${1:-}" in
uninstall) uninstall_qemu ;;
*) install_qemu ;;
esac
+9 -1
View File
@@ -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
View File
@@ -7,4 +7,13 @@ install_btop() {
spawn "Installing btop" sudo apt install -y btop
}
install_btop
uninstall_btop() {
command -v btop &>/dev/null || { log "btop not installed"; return 0; }
spawn "Removing btop" sudo apt purge -y btop
spawn "Cleaning up dependencies" sudo apt autoremove -y
}
case "${1:-}" in
uninstall) uninstall_btop ;;
*) install_btop ;;
esac
+12 -1
View File
@@ -16,4 +16,15 @@ install_localsend() {
spawn "Installing localsend" sudo flatpak install -y flathub org.localsend.localsend_app
}
install_localsend
uninstall_localsend() {
if ! flatpak list 2>/dev/null | grep -q org.localsend.localsend_app; then
log "localsend not installed"
return 0
fi
spawn "Removing localsend" sudo flatpak uninstall -y org.localsend.localsend_app
}
case "${1:-}" in
uninstall) uninstall_localsend ;;
*) install_localsend ;;
esac
+22
View File
@@ -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
View File
@@ -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"
+58
View File
@@ -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
View File
@@ -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"
+80 -17
View File
@@ -25,9 +25,60 @@ _pos_commands() {
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 ──────────────────────────────────────────────────
usage() {
cat <<'EOF'
local cats
cats="$(_pos_category_list)"
cat <<EOF
pos — Personal OS Toolkit
A unified CLI for network, docker, media, system,
@@ -37,37 +88,41 @@ USAGE
pos <category> <command> [args]
CATEGORIES
network ip | checkport | scan
docker ps | compose
media mp3 | mp4
system firewall
ssh load-keys
vbox create | enter | stop | start | rm | ls
$cats
EXAMPLES
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 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 compose ls List available ScaleTail services
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 mp4 <url> Download video as MP4
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 vbox create lab1 Create disposable Docker VM
pos vbox create lab1 --dir . Create VM using current directory
pos vbox enter lab1 Shell into a Docker VM
pos vbox ls List Docker VMs
pos usb server --ls List USB devices + connected clients
pos communication telegram --send "Backup done"
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
pos help <command> Show help for a command
pos --help Show this help
pos <category> --help Show a category's commands
pos help <command> Show help for a command
pos --help Show this help
LEGACY WRAPPERS
wr-ip, wr-checkport, wr-scan-ping, wr-docker,
@@ -86,12 +141,13 @@ esac
if [ "${1:-}" = "help" ]; then
shift
[ $# -eq 0 ] && usage
cmd="pos-${1// /-}"
cmd="pos-$*"
cmd="${cmd// /-}"
if command -v "$cmd" &>/dev/null; then
exec "$cmd" --help
fi
[ -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
exit 1
fi
@@ -100,6 +156,13 @@ fi
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 ──────────────────────────────────────────────
LOG_DIR="$HOME/.local/share/linux_post_install/logs"
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"; }
# 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
cmd="pos"
+140
View File
@@ -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
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
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"
SCALE_DIR="/usr/local/share/linux_post_install/scale-tail/services"
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
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"
usage() {
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
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"
usage() {
+16 -15
View File
@@ -1,16 +1,17 @@
#!/usr/bin/env bash
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"
usage() {
cat <<EOF
Usage:
pos vbox create <name> [image] [--dir <path>]
pos vbox enter <name>
pos vbox stop <name>
pos vbox start <name>
pos vbox rm <name>
pos vbox ls
pos docker vbox create <name> [image] [--dir <path>]
pos docker vbox enter <name>
pos docker vbox stop <name>
pos docker vbox start <name>
pos docker vbox rm <name>
pos docker vbox ls
Manage disposable Docker containers as lightweight VMs.
@@ -22,15 +23,15 @@ Options:
Use "." for current directory
Examples:
pos vbox create lab1
pos vbox create lab1 --dir .
pos vbox create lab1 --dir /mnt/data/lab1
pos vbox create kali kalilinux/kali-rolling
pos vbox enter lab1
pos vbox stop lab1
pos vbox start lab1
pos vbox rm lab1
pos vbox ls
pos docker vbox create lab1
pos docker vbox create lab1 --dir .
pos docker vbox create lab1 --dir /mnt/data/lab1
pos docker vbox create kali kalilinux/kali-rolling
pos docker vbox enter lab1
pos docker vbox stop lab1
pos docker vbox start lab1
pos docker vbox rm lab1
pos docker vbox ls
EOF
exit 0
}
+7
View File
@@ -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
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: media mp3 — Download audio as MP3 (yt-dlp)
usage() {
cat <<EOF
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: media mp4 — Download video as MP4 (interactive format select)
usage() {
cat <<EOF
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: network checkport — Check TCP port connectivity
usage() {
cat <<EOF
+93
View File
@@ -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
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: network ip — Show interfaces, routes, public IP + location
usage() {
cat <<EOF
@@ -42,5 +43,27 @@ SEP
echo "Public IP"
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
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: network scan — Parallel ping sweep of CIDR
usage() {
cat <<EOF
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: ssh load-keys — Load all SSH keys into the agent
usage() {
cat <<EOF
+117
View File
@@ -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
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# POS: system firewall — Interactive UFW management
IFS=$'\n\t'
if [[ $EUID -ne 0 ]]; then
+218
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
#!/usr/bin/env bash
exec pos vbox "$@"
exec pos docker vbox "$@"
+34 -6
View File
@@ -1,6 +1,13 @@
#!/usr/bin/env bash
# Bash completion for pos — dynamically discovers pos-* subcommands
# 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() {
local cur prev words cword
@@ -45,7 +52,7 @@ _pos() {
_pos_complete_subcats() {
local cat="${words[1]}"
COMPREPLY=($(compgen -W "${cat_cmds[$cat]:-}" -- "$cur"))
COMPREPLY=($(compgen -W "${cat_cmds[$cat]:-} --help" -- "$cur"))
}
_pos_complete_compose_services() {
@@ -63,7 +70,7 @@ _pos() {
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"))
}
@@ -73,6 +80,11 @@ _pos() {
COMPREPLY=($(compgen -W "$names" -- "$cur"))
}
_pos_complete_flags() {
local tool="$1"
COMPREPLY=($(compgen -W "${_pos_flags[$tool]:-} --help" -- "$cur"))
}
# ── Dispatch ───────────────────────────────────────────────
case "${#words[@]}" in
2)
@@ -93,8 +105,20 @@ _pos() {
;;
esac
;;
vbox-*)
_pos_complete_vbox_cmds
docker-vbox)
_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
;;
@@ -107,8 +131,12 @@ _pos() {
;;
esac
;;
vbox-create|vbox-enter|vbox-stop|vbox-start|vbox-rm)
_pos_complete_docker_vbox_names
docker-vbox)
case "${words[3]}" in
create|enter|stop|start|rm)
_pos_complete_docker_vbox_names
;;
esac
;;
esac
;;
-1
View File
@@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK8u4mvN1oWUGjWZCjUT0742u6AJEIHfi+PAQgZuNnPv
+26
View File
@@ -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"
+18
View File
@@ -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&current_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"
+49 -1
View File
@@ -12,9 +12,11 @@ if [ "$NO_COLOR" -eq 1 ]; then
fi
source "$(dirname "$0")/lib/common.sh"
source "$(dirname "$0")/lib/flags.sh"
DRY_RUN=0
RUN_APPS=0
RUN_FEATURES=0
SKIP_PHASES=""
STEPS_SPEC=""
@@ -27,6 +29,7 @@ Bootstrap a fresh Debian/Ubuntu install.
Options:
--apps Run interactive app picker after core install
--full Core install + all optional apps (non-interactive)
--feature Install features/ scripts (prompts before overwriting)
--dry-run Show what would be done without executing
--skip <phase> Skip a phase (repeatable):
preinstall, scripts, postinstall, scalepoint, apps
@@ -34,6 +37,11 @@ Options:
(1=preinstall, 2=scripts, 3=postinstall, 4=scalepoint)
--no-color Disable colored output
-h, --help Show this help message
Uninstall optional apps later with:
./apps/install.sh --uninstall # interactive uninstall selection
./apps/install.sh --uninstall --all # uninstall everything
./apps/install.sh --uninstall <app>... # uninstall specific apps
EOF
exit 0
}
@@ -42,6 +50,7 @@ while [[ $# -gt 0 ]]; do
case "$1" in
--apps) RUN_APPS=1; shift ;;
--full) RUN_APPS=2; shift ;;
--feature) RUN_FEATURES=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--skip)
[ -z "${2:-}" ] && err "Missing value for --skip"
@@ -103,7 +112,46 @@ if should_run 2 scripts; then
count=$((count + 1))
done
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
# ── Phase 3: postinstall ───────────────────────────────────────
+60
View File
@@ -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
}
+7
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/lib/common.sh"
source "$(dirname "$0")/lib/flags.sh"
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
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 "$svc_name"
done
+3 -1
View File
@@ -26,12 +26,14 @@ while [[ $# -gt 0 ]]; do
done
PACKAGES=(
git curl wget vim nano tmux tree jq
git curl wget bc 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
)
spawn "apt update" sudo apt update
+143
View File
@@ -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"
+8
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Run POS Entertainment Hourly
[Timer]
OnBootSec=5min
OnUnitActiveSec=1h
Persistent=true
[Install]
WantedBy=timers.target
+67
View File
@@ -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.
# ────────────────────────────────────────────────────────────────
+54
View File
@@ -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
+52
View File
@@ -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.
+2044
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Start wihotspot-gui
/usr/local/bin/wihotspot-gui
BIN
View File
Binary file not shown.