.
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
# Python (if src/ or pyinstaller is ever used)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.spec
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker Compose — user's active stacks and secrets
|
||||||
|
compose/custom/
|
||||||
|
compose/config.env
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "compose/scale-tail"]
|
||||||
|
path = compose/scale-tail
|
||||||
|
url = https://github.com/tailscale-dev/ScaleTail.git
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
# AGENT Context — myLinux Project
|
||||||
|
|
||||||
|
> **Purpose:** Single-source context document so any AI agent can understand the project, navigate the codebase, and make correct contributions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Project Overview
|
||||||
|
|
||||||
|
**myLinux** is a personal bootstrap and homelab toolkit for Debian/Ubuntu. One command turns a bare install into a fully productive machine:
|
||||||
|
|
||||||
|
- Automated system package installation (25+ packages)
|
||||||
|
- A unified CLI (`pos`) for network, Docker, media, system, and SSH tasks
|
||||||
|
- Optional desktop application installers (15 apps)
|
||||||
|
- Docker Compose service management via ScaleTail templates (119+ self-hosted services with Tailscale sidecar)
|
||||||
|
- Systemd service management for boot-time automation
|
||||||
|
|
||||||
|
**Repository:** `https://github.com/IFindMe/myLinux`
|
||||||
|
**Target OS:** Debian / Ubuntu (uses `apt`)
|
||||||
|
**Shell:** Bash (`#!/usr/bin/env bash`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
myLinux/
|
||||||
|
├── install.sh # Main orchestrator — entry point
|
||||||
|
├── preinstall.sh # Phase 1: system packages via apt + yt-dlp
|
||||||
|
├── postinstall.sh # Phase 3: PATH, bash completion, systemd services
|
||||||
|
│
|
||||||
|
├── lib/
|
||||||
|
│ └── common.sh # Shared library (colors, logging, spinner, timer, run)
|
||||||
|
│
|
||||||
|
├── 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)
|
||||||
|
│ ├── wr-* # Legacy wrappers → pos (backward compat)
|
||||||
|
│ ├── mp3, mp4, vbox # Legacy convenience wrappers → pos
|
||||||
|
│ └── ssh-load-all # Legacy wrapper → pos ssh load-keys
|
||||||
|
│
|
||||||
|
├── apps/ # Optional desktop app installers (by category)
|
||||||
|
│ ├── install.sh # Interactive picker / orchestrator
|
||||||
|
│ ├── browsers/
|
||||||
|
│ │ └── brave.sh # Brave Browser (APT repo)
|
||||||
|
│ ├── development/
|
||||||
|
│ │ ├── opencode.sh # opencode AI agent (official script)
|
||||||
|
│ │ └── vscode.sh # VS Code (Microsoft APT repo)
|
||||||
|
│ ├── media/
|
||||||
|
│ │ ├── obs.sh # OBS Studio (apt)
|
||||||
|
│ │ ├── scrcpy.sh # scrcpy Android mirror (GitHub release)
|
||||||
|
│ │ └── vlc.sh # VLC media player (apt)
|
||||||
|
│ ├── networking/
|
||||||
|
│ │ ├── netbird.sh # NetBird VPN (official script)
|
||||||
|
│ │ ├── tailscale.sh # Tailscale VPN (official script)
|
||||||
|
│ │ └── zerotier.sh # ZeroTier VPN (official script)
|
||||||
|
│ ├── remote-access/
|
||||||
|
│ │ ├── termius.sh # Termius SSH client (.deb)
|
||||||
|
│ │ └── vnc-viewer.sh # TigerVNC Viewer (apt)
|
||||||
|
│ ├── system/
|
||||||
|
│ │ ├── docker.sh # Docker Engine (get.docker.com)
|
||||||
|
│ │ └── qemu.sh # QEMU + libvirt + KVM (apt)
|
||||||
|
│ └── utilities/
|
||||||
|
│ ├── affine.sh # AFFiNE knowledge base (AppImage)
|
||||||
|
│ ├── btop.sh # btop resource monitor (apt)
|
||||||
|
│ └── localsend.sh # LocalSend (flatpak)
|
||||||
|
│
|
||||||
|
├── completions/
|
||||||
|
│ └── pos.bash # Bash tab-completion for the pos CLI
|
||||||
|
│
|
||||||
|
├── compose/
|
||||||
|
│ └── scale-tail/ # Git submodule → ScaleTail templates (119+ services)
|
||||||
|
│
|
||||||
|
├── systemd/
|
||||||
|
│ ├── autostart.service # Runs autostart.sh on boot
|
||||||
|
│ └── ssh-agent.service # System-wide SSH agent socket
|
||||||
|
│
|
||||||
|
├── README.md # User-facing documentation
|
||||||
|
├── DEV.md # Developer guide
|
||||||
|
├── .gitignore # Excludes secrets, Python artifacts, OS files
|
||||||
|
└── .gitmodules # Submodule: compose/scale-tail → ScaleTail
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Installation Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User runs: ./install.sh [--apps|--full|--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)
|
||||||
|
│
|
||||||
|
├─ 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
|
||||||
|
│
|
||||||
|
├─ Phase 4: ScaleTail clone
|
||||||
|
│ └─ Shallow-clones ScaleTail templates to /usr/local/share/mylinux/scale-tail
|
||||||
|
│
|
||||||
|
└─ [if --apps or --full]: apps/install.sh
|
||||||
|
└─ Interactive picker (or --all for non-interactive)
|
||||||
|
```
|
||||||
|
|
||||||
|
**After install, the repo can be deleted** — all tools live in `/usr/local/bin/` and templates in `/usr/local/share/mylinux/`.
|
||||||
|
|
||||||
|
### install.sh Flags
|
||||||
|
|
||||||
|
| Flag | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `--apps` | Run interactive app picker after core install |
|
||||||
|
| `--full` | Core install + all apps (non-interactive) |
|
||||||
|
| `--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` |
|
||||||
|
| `--no-color` | Disable colored output |
|
||||||
|
|
||||||
|
### pos Output Logging
|
||||||
|
|
||||||
|
All non-interactive `pos` commands log output to `~/.local/share/mylinux/logs/`:
|
||||||
|
- Per-command files: `YYYYMMDD_HHMMSS_pos_<cmd>.log` (full stdout+stderr)
|
||||||
|
- Main log: `pos.log` (command + timestamp + exit code for every invocation)
|
||||||
|
- Interactive commands (`system-firewall`, `media-mp4`) only log invocation, not output
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The `pos` CLI System
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
`bin/pos` is the main dispatcher. It:
|
||||||
|
1. Scans its own directory for all executable `pos-*` files
|
||||||
|
2. Extracts category-subcommand names from filenames
|
||||||
|
3. Uses variable-length argument matching to find the right script
|
||||||
|
|
||||||
|
**Example:** `pos docker compose up jellyfin`
|
||||||
|
- Tries `pos-docker-compose-up-jellyfin` (not found)
|
||||||
|
- Tries `pos-docker-compose-up` (not found)
|
||||||
|
- Finds `pos-docker-compose` (runs with args `up jellyfin`)
|
||||||
|
|
||||||
|
### Available Commands
|
||||||
|
|
||||||
|
| Category | Command | Script | Description |
|
||||||
|
|----------|---------|--------|-------------|
|
||||||
|
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP |
|
||||||
|
| network | checkport | `pos-network-checkport` | Check TCP port connectivity |
|
||||||
|
| 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 |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
### 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-vbox` manages disposable Docker containers as lightweight VMs:
|
||||||
|
|
||||||
|
- **Container labeling:** All created containers get `mylinux.vbox=true` label
|
||||||
|
- **`ls` filtering:** `docker ps --filter label=mylinux.vbox=true` — only shows vbox-managed containers
|
||||||
|
- **Post-create prompt:** After `create`, asks "Enter now? [Y/n]" using `confirm` helper
|
||||||
|
- **Working dir detection:** `enter` auto-detects bind mount path from container labels
|
||||||
|
- **Custom dirs:** `--dir <path>` or `--dir .` for current directory
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Shared Library — `lib/common.sh`
|
||||||
|
|
||||||
|
Sourced by most scripts. Provides:
|
||||||
|
|
||||||
|
| Function | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `log "msg"` | Green `[+]` status message |
|
||||||
|
| `warn "msg"` | Yellow `[!]` warning |
|
||||||
|
| `err "msg"` | Red `ERROR:` + exit 1 |
|
||||||
|
| `ok "msg"` | Green `OK` prefix |
|
||||||
|
| `section "title"` | Cyan-bordered section header |
|
||||||
|
| `step N T "msg"` | Numbered step header (e.g., `[1/4] Installing`) |
|
||||||
|
| `run cmd` | Executes command, respects `$DRY_RUN` |
|
||||||
|
| `spawn "msg" cmd` | Runs with animated braille spinner, elapsed time, OK/FAIL status |
|
||||||
|
| `timer_start` / `timer_stop` | Elapsed time tracking |
|
||||||
|
| `confirm "prompt" [default]` | y/N or Y/n prompt |
|
||||||
|
|
||||||
|
**Auto-detects TTY** — disables colors when piped.
|
||||||
|
|
||||||
|
**Source pattern:**
|
||||||
|
```bash
|
||||||
|
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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Docker Compose / ScaleTail
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
ScaleTail provides 119+ Docker Compose templates with a Tailscale sidecar pattern (`network_mode: service:tailscale`). Each service gets a `tail-xxxxx.ts.net` URL with optional automatic HTTPS.
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/share/mylinux/scale-tail/ # Templates (git repo)
|
||||||
|
└── services/<name>/
|
||||||
|
├── compose.yaml
|
||||||
|
└── .env
|
||||||
|
|
||||||
|
~/.config/mylinux/compose.env # Global defaults (TS_AUTHKEY, TZ, DNS_SERVER, SERVICES_BASE)
|
||||||
|
|
||||||
|
/srv/<service>/ # Active deployments (default base)
|
||||||
|
├── compose.yaml # From template (refreshed on update)
|
||||||
|
├── .env # User config (preserved across updates)
|
||||||
|
├── config/
|
||||||
|
└── data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `pos docker compose ls` | List all available ScaleTail services |
|
||||||
|
| `pos docker compose up <svc>` | Deploy service to SERVICES_BASE |
|
||||||
|
| `pos docker compose down <svc>` | Stop a deployed service |
|
||||||
|
| `pos docker compose restart <svc>` | Restart a service |
|
||||||
|
| `pos docker compose logs <svc> [-f]` | View/follow logs |
|
||||||
|
| `pos docker compose update` | Pull latest templates, refresh compose.yaml (preserves .env) |
|
||||||
|
| `pos docker compose config set K=V` | Set global config value |
|
||||||
|
| `pos docker compose config show` | Display current config |
|
||||||
|
|
||||||
|
### Global Config Keys
|
||||||
|
|
||||||
|
- `TS_AUTHKEY` — Tailscale auth key (required)
|
||||||
|
- `TZ` — Timezone
|
||||||
|
- `DNS_SERVER` — Custom DNS
|
||||||
|
- `SERVICES_BASE` — Deployment root (default: `/srv`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Optional Apps (`apps/`)
|
||||||
|
|
||||||
|
### How They Work
|
||||||
|
|
||||||
|
- `apps/install.sh` auto-discovers all `apps/<category>/*.sh` files (excluding itself)
|
||||||
|
- Three modes: interactive (default), `--all`, or specific app names as arguments
|
||||||
|
- Interactive TUI groups apps by category with section headers
|
||||||
|
- Each app script is standalone, idempotent, sources `lib/common.sh`
|
||||||
|
|
||||||
|
### Installation Methods
|
||||||
|
|
||||||
|
| Method | Apps |
|
||||||
|
|--------|------|
|
||||||
|
| `apt install` | btop, obs, vlc, vnc-viewer, qemu |
|
||||||
|
| APT repo (GPG + repo) | brave, vscode |
|
||||||
|
| Official `curl \| sh` | docker, tailscale, netbird, zerotier, opencode |
|
||||||
|
| AppImage | affine |
|
||||||
|
| GitHub release binary | scrcpy |
|
||||||
|
| Flatpak | localsend |
|
||||||
|
| .deb package | termius |
|
||||||
|
|
||||||
|
### Adding a New App
|
||||||
|
|
||||||
|
1. Create `apps/<name>.sh` following the template in DEV.md
|
||||||
|
2. It auto-appears in the interactive picker — no registration needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Systemd Services
|
||||||
|
|
||||||
|
| Service | File | Purpose |
|
||||||
|
|---------|------|---------|
|
||||||
|
| `ssh-agent.service` | `systemd/ssh-agent.service` | System-wide SSH agent, socket at `/run/ssh-agent/socket` |
|
||||||
|
| `autostart.service` | `systemd/autostart.service` | Runs `autostart.sh` on boot |
|
||||||
|
|
||||||
|
All `.service` files in `systemd/` are automatically copied to `/etc/systemd/system/` and enabled by `postinstall.sh`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Configuration Files
|
||||||
|
|
||||||
|
### Gitignored Secrets
|
||||||
|
|
||||||
|
- `config/rclone.conf` — rclone remote config (OAuth tokens)
|
||||||
|
- `config/authorized_keys` — SSH public keys
|
||||||
|
|
||||||
|
### Runtime Config
|
||||||
|
|
||||||
|
- `~/.config/mylinux/compose.env` — Docker Compose global defaults
|
||||||
|
- `~/.bashrc` — Modified by postinstall (PATH, bash completion)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Coding Conventions
|
||||||
|
|
||||||
|
### Script Standards
|
||||||
|
|
||||||
|
- **Shebang:** `#!/usr/bin/env bash`
|
||||||
|
- **Strict mode:** `set -euo pipefail`
|
||||||
|
- **Help:** Every script accepts `-h`/`--help` via `case` pattern
|
||||||
|
- **Idempotency:** Check existence before creating/modifying
|
||||||
|
- **Exit codes:** 0 = success, 1 = error
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
- `pos-<category>-<command>` — canonical tool names
|
||||||
|
- `wr-*` — legacy wrappers
|
||||||
|
- `apps/<category>/<name>.sh` — optional app installers
|
||||||
|
- Hyphens for word separation, lowercase always
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- `command -v <tool> &>/dev/null` to check tool availability
|
||||||
|
- `set -euo pipefail` for fail-fast
|
||||||
|
- `err()` for fatal errors, `warn()` for non-fatal
|
||||||
|
- Confirmation prompts for destructive actions
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Never hardcode secrets in scripts
|
||||||
|
- Use `chmod 600` for sensitive files
|
||||||
|
- Validate user input before shell commands
|
||||||
|
- Use `sudo` only where necessary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Development Workflow
|
||||||
|
|
||||||
|
### Adding a New App
|
||||||
|
|
||||||
|
1. Create `apps/<category>/<name>.sh` following the template in DEV.md
|
||||||
|
2. It auto-appears in the interactive picker — no registration needed
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Syntax check all scripts
|
||||||
|
for f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do
|
||||||
|
bash -n "$f" || echo "FAIL: $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ShellCheck linting
|
||||||
|
shellcheck bin/my-script
|
||||||
|
|
||||||
|
# Test in Docker
|
||||||
|
docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash
|
||||||
|
# inside: cd /repo && ./install.sh
|
||||||
|
|
||||||
|
# Test apps interactively
|
||||||
|
./apps/install.sh --all
|
||||||
|
./apps/install.sh docker vscode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commit Conventions
|
||||||
|
|
||||||
|
Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Key File Quick Reference
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Common Tasks for Agents
|
||||||
|
|
||||||
|
| Task | Where to Edit |
|
||||||
|
|------|---------------|
|
||||||
|
| Add a new CLI tool | Create `bin/pos-<cat>-<cmd>`, add deps in `preinstall.sh` |
|
||||||
|
| Add a new app installer | Create `apps/<name>.sh` (auto-discovered) |
|
||||||
|
| Add a systemd service | Create `systemd/<name>.service` (auto-installed by postinstall) |
|
||||||
|
| 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 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` |
|
||||||
|
| Add a secret config file | Add to `config/`, update `.gitignore`, add copy logic in `postinstall.sh` |
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
# Development Guide
|
||||||
|
|
||||||
|
How this repo works, how to add features, and what to keep in mind when editing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
### Three-Phase Installation
|
||||||
|
|
||||||
|
The installer runs in three sequential phases:
|
||||||
|
|
||||||
|
```
|
||||||
|
install.sh
|
||||||
|
│
|
||||||
|
┌───────────┼───────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
preinstall.sh bin/* postinstall.sh
|
||||||
|
(packages) → /usr/local/bin (config + services)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Phase | Script | Responsibility |
|
||||||
|
|-------|--------|----------------|
|
||||||
|
| Pre | `preinstall.sh` | System packages, apt repositories, global binaries (yt-dlp) |
|
||||||
|
| Install | `install.sh` | Copies everything in `bin/` to `/usr/local/bin` with `chmod 755` |
|
||||||
|
| Post | `postinstall.sh` | User config (SSH, rclone), `~/.bashrc`, systemd services |
|
||||||
|
|
||||||
|
Each phase is independent and is only run if the corresponding file exists.
|
||||||
|
|
||||||
|
### Script Categories
|
||||||
|
|
||||||
|
| Directory | Purpose | Installed To |
|
||||||
|
|-----------|---------|--------------|
|
||||||
|
| `bin/` | Daily-use tools and wrappers | `/usr/local/bin/` |
|
||||||
|
| `apps/<category>/` | Optional desktop apps (by category) | run on demand |
|
||||||
|
| `lib/` | Shared library (`common.sh`) | sourced at build time |
|
||||||
|
<<<<<<< HEAD
|
||||||
|
| `config/` | Static config files + SSH authorized_keys | `~/.config/<app>/` (via postinstall) |
|
||||||
|
=======
|
||||||
|
| `config/` | Static config files (gitignored — user adds their own) | `~/.config/<app>/` (via postinstall) |
|
||||||
|
>>>>>>> bba577c (Initial commit)
|
||||||
|
| `compose/` | ScaleTail templates (dev reference only) | cloned to `/usr/local/share/mylinux/scale-tail` on install |
|
||||||
|
| `systemd/` | Systemd service unit files | `/etc/systemd/system/` (via postinstall) |
|
||||||
|
|
||||||
|
### Key Files Added
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
| `.gitignore` | Prevents secrets (rclone tokens) and build artifacts from being committed |
|
||||||
|
| `config/authorized_keys` | SSH public keys read by `postinstall.sh` (replaces hardcoded key) |ls
|
||||||
|
|
||||||
|
=======
|
||||||
|
| `.gitignore` | Prevents secrets (rclone tokens, SSH keys) and build artifacts from being committed |
|
||||||
|
>>>>>>> bba577c (Initial commit)
|
||||||
|
| `~/.config/mylinux/compose.env` | Global Docker Compose defaults (`TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE`) — created by `wr-compose config` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Add a New Tool
|
||||||
|
|
||||||
|
### 1. Create the script in `bin/`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Use the shared library for colors and helpers (preferred)
|
||||||
|
source "$(dirname "$0")/../lib/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: my-tool <argument>
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- script logic ---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conventions to follow:**
|
||||||
|
|
||||||
|
- **Shebang:** `#!/usr/bin/env bash` (portable across distros)
|
||||||
|
- **Strict mode:** `set -euo pipefail` at the top
|
||||||
|
- **`--help` flag:** all tools must accept `-h` / `--help` — use the `case ... esac` pattern above
|
||||||
|
- **Shared library:** source `lib/common.sh` from any script in `bin/` or `apps/` for consistent colors, logging (`log`, `warn`, `err`, `ok`), spinners (`spawn`), and dry-run support (`run`). Use `spawn "message" command` for long-running installs.
|
||||||
|
- **Fallback (no lib):** if sourcing `common.sh` is not desired, inline:
|
||||||
|
```bash
|
||||||
|
log() { echo "[+] $*"; }
|
||||||
|
warn() { echo "[!] $*"; }
|
||||||
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
```
|
||||||
|
- **Exit codes:** `0` for success, `1` for error
|
||||||
|
|
||||||
|
### 2. Add system dependencies (if any)
|
||||||
|
|
||||||
|
Open `preinstall.sh` and add the package name to the `PACKAGES` array:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PACKAGES=(
|
||||||
|
...
|
||||||
|
your-package
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add runtime configuration (if any)
|
||||||
|
|
||||||
|
If the tool needs a config file:
|
||||||
|
- Place the file in `config/`
|
||||||
|
- Add copy logic in `postinstall.sh`
|
||||||
|
|
||||||
|
If the file contains secrets (tokens, keys):
|
||||||
|
- Add it to `.gitignore`
|
||||||
|
- Document in README how to create it manually
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
### 4. Add SSH keys (if needed)
|
||||||
|
|
||||||
|
Place public keys in `config/authorized_keys` (one per line).
|
||||||
|
`postinstall.sh` reads from this file automatically.
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> bba577c (Initial commit)
|
||||||
|
### 5. Update README.md
|
||||||
|
|
||||||
|
Add a section under **Tools Reference** following the existing format.
|
||||||
|
|
||||||
|
### 6. Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Syntax check
|
||||||
|
bash -n bin/your-tool
|
||||||
|
|
||||||
|
# ShellCheck linting
|
||||||
|
shellcheck bin/your-tool
|
||||||
|
|
||||||
|
# Run directly
|
||||||
|
./bin/your-tool --help
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Edit an Existing Tool
|
||||||
|
|
||||||
|
1. **Find the script** — all tools live in `bin/`
|
||||||
|
2. **Understand the contract** — what args does it expect? What does it print? What exit codes?
|
||||||
|
3. **Make the change** — keep it idempotent if possible (running twice = same result)
|
||||||
|
4. **Update README** if usage, output, or behaviour changed
|
||||||
|
5. **Run `shellcheck`** on the modified file:
|
||||||
|
```bash
|
||||||
|
shellcheck bin/your-tool
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Add a New App
|
||||||
|
|
||||||
|
App installers live in `apps/<category>/` and follow a simple pattern. Each is a standalone script that can be run independently.
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_myapp() {
|
||||||
|
command -v myapp &>/dev/null && { log "myapp already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing myapp" sudo apt install -y myapp
|
||||||
|
}
|
||||||
|
|
||||||
|
install_myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: app scripts are now in `apps/<category>/`, so the source path to `common.sh` is two levels up (`../../lib/common.sh`).
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
|
||||||
|
- **Shebang:** `#!/usr/bin/env bash`
|
||||||
|
- **Strict mode:** `set -euo pipefail`
|
||||||
|
- **Shared library:** always source `lib/common.sh` from the app directory
|
||||||
|
- **Idempotent:** check `command -v` before installing; skip if present
|
||||||
|
- **Method:** standardize on official repos/scripts over PPAs or third-party
|
||||||
|
- **APT packages** → `sudo apt install -y <pkg>` wrapped in `spawn`
|
||||||
|
- **Official scripts** → `curl ... | sh` inside `spawn`
|
||||||
|
- **Flatpak** → `flatpak install -y flathub <app-id>` inside `spawn`
|
||||||
|
- **`.deb` files** → download to temp and `sudo apt install -y ./file.deb` inside `spawn`
|
||||||
|
- **Groups:** `usermod` commands print a re-login reminder (`log "Log out and back in for group changes to take effect"`)
|
||||||
|
|
||||||
|
### Adding to the picker
|
||||||
|
|
||||||
|
`apps/install.sh` auto-discovers all `apps/<category>/*.sh` files (excluding itself). Just create the script in the appropriate category subdirectory and it will appear in the interactive prompt under that category.
|
||||||
|
|
||||||
|
Categories: `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Idempotency
|
||||||
|
|
||||||
|
Scripts should be safe to run multiple times:
|
||||||
|
- Check if something exists before creating it
|
||||||
|
- Use `>>` with checks (grep for existing content) instead of blindly appending
|
||||||
|
- Don't overwrite configs that the user may have customized
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fail fast
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Check for required commands
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo "docker not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check arguments
|
||||||
|
if [[ -z "${1:-}" ]]; then
|
||||||
|
echo "Usage: my-tool <argument>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
### Portability
|
||||||
|
|
||||||
|
This repo targets **Debian** and **Ubuntu**. Keep in mind:
|
||||||
|
- Use `apt` not `apt-get` unless you need non-interactive guarantees
|
||||||
|
- Assume `bash` is at `/usr/bin/env bash`
|
||||||
|
- Prefer POSIX-safe patterns when possible
|
||||||
|
- Check for command availability with `command -v`
|
||||||
|
|
||||||
|
### Dry-run support
|
||||||
|
|
||||||
|
Scripts that make changes (`install.sh`, `preinstall.sh`) support `--dry-run`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh --dry-run # preview without executing
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the `run()` helper pattern:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
run() {
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
log "(dry-run) $*"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run sudo apt install -y git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **Never hardcode secrets** in scripts (SSH keys, API tokens, passwords) — put them in `config/` files that are `.gitignore`d
|
||||||
|
- Use `chmod 600` for sensitive files (SSH keys, rclone config)
|
||||||
|
- Validate user input before using it in shell commands
|
||||||
|
- Use `sudo` only where necessary; don't run the whole script as root if only one command needs elevation
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
- Prefix personal wrappers with `wr-` (e.g., `wr-ip`, `wr-docker`)
|
||||||
|
- Keep names lowercase, use hyphens for word separation
|
||||||
|
- Name should hint at the tool's purpose (`wr-scan-ping`, `wr-checkport`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working with Systemd
|
||||||
|
|
||||||
|
### Adding a new service
|
||||||
|
|
||||||
|
1. Create `systemd/<name>.service`
|
||||||
|
2. postinstall.sh automatically copies all `*.service` files to `/etc/systemd/system/` and enables them
|
||||||
|
|
||||||
|
Service file template:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=My Service
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/bin/your-script.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working with Config Files
|
||||||
|
|
||||||
|
1. Place the file in `config/`
|
||||||
|
2. Add a section to `postinstall.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ -f config/your-config.conf ]; then
|
||||||
|
mkdir -p "$HOME/.config/your-app"
|
||||||
|
cp config/your-config.conf "$HOME/.config/your-app/your-config.conf"
|
||||||
|
chmod 600 "$HOME/.config/your-app/your-config.conf"
|
||||||
|
echo "Installed your-config.conf"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working with Docker Compose
|
||||||
|
|
||||||
|
The installer clones [ScaleTail](https://github.com/tailscale-dev/ScaleTail) templates to `/usr/local/share/mylinux/scale-tail/` — a library of 119+ self-hosted services with a **Tailscale sidecar** pattern. Each service runs with `network_mode: service:tailscale`, gets a `tail-xxxxx.ts.net` URL, and optional automatic HTTPS via Tailscale Serve or Funnel.
|
||||||
|
|
||||||
|
### Architecture (after install)
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/share/mylinux/scale-tail/ # ScaleTail templates (git repo)
|
||||||
|
└── services/<name>/
|
||||||
|
├── compose.yaml # Service definition (Tailscale + app containers)
|
||||||
|
└── .env # Template variables (SERVICE, IMAGE_URL, TS_AUTHKEY, TZ, ...)
|
||||||
|
|
||||||
|
~/.config/mylinux/compose.env # Global defaults — set via wr-compose config
|
||||||
|
|
||||||
|
<SERVICES_BASE>/<name>/ # Active deployments (default: /srv/<name>)
|
||||||
|
├── compose.yaml # Copied from template (refreshed on wr-compose update)
|
||||||
|
├── .env # Your real config — preserved across updates
|
||||||
|
├── config/ # Service configuration data
|
||||||
|
└── data/ # Service persistent data
|
||||||
|
```
|
||||||
|
|
||||||
|
### `wr-compose` commands
|
||||||
|
|
||||||
|
| Command | Behaviour |
|
||||||
|
|---------|-----------|
|
||||||
|
| `wr-compose up <service>` | Deploys service to `$SERVICES_BASE/<service>/` (default: `/srv`), creates `config/` + `data/` dirs, generates `.env` from global config (prompts for `TS_AUTHKEY` if empty), runs `docker compose up -d` |
|
||||||
|
| `wr-compose down <service>` | Runs `docker compose down` in the service directory |
|
||||||
|
| `wr-compose update` | `git pull` in ScaleTail templates dir, then re-copies `compose.yaml` into all deployed directories — `.env` files are left untouched |
|
||||||
|
| `wr-compose config set K=V` | Persists a value in `~/.config/mylinux/compose.env` (e.g. `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE`) |
|
||||||
|
|
||||||
|
### Portable `.env` design
|
||||||
|
|
||||||
|
- **Global**: `~/.config/mylinux/compose.env` — one place for `TS_AUTHKEY`, `TZ`, `DNS_SERVER`, `SERVICES_BASE`.
|
||||||
|
- **Per-service**: `<SERVICES_BASE>/<service>/.env` — generated from the ScaleTail template on first deploy, with empty values filled from the global config.
|
||||||
|
- **On update**: `wr-compose update` refreshes only `compose.yaml` from the templates; `.env` files are preserved.
|
||||||
|
- **Services path**: set `SERVICES_BASE` to any directory (e.g. `/srv`) via `wr-compose config set SERVICES_BASE=/srv`. Defaults to `/srv`.
|
||||||
|
|
||||||
|
This means `wr-compose` works anywhere — no repo clone needed after install. Just set `TS_AUTHKEY` once and deploy.
|
||||||
|
|
||||||
|
### Contributing upstream
|
||||||
|
|
||||||
|
ScaleTail provides a [service template](https://github.com/tailscale-dev/ScaleTail/tree/main/templates/service-template). To add a service:
|
||||||
|
|
||||||
|
1. Fork ScaleTail and add your service under `services/<name>/`
|
||||||
|
2. Submit a PR upstream
|
||||||
|
3. Changes are picked up by `wr-compose update`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Guidelines
|
||||||
|
|
||||||
|
- Use conventional commit prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||||
|
- Explain *why* the change was made, not just *what* changed
|
||||||
|
- Keep commits focused — one logical change per commit
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat: add wr-mytool for monitoring disk usage
|
||||||
|
fix: wr-ip fails when no default route exists
|
||||||
|
docs: add example output for wr-scan-ping
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Syntax-check a script without running it
|
||||||
|
bash -n bin/my-script
|
||||||
|
bash -n apps/utilities/myapp.sh
|
||||||
|
|
||||||
|
# ShellCheck linting
|
||||||
|
shellcheck bin/my-script
|
||||||
|
shellcheck apps/utilities/myapp.sh
|
||||||
|
|
||||||
|
# Quick syntax check all scripts
|
||||||
|
for f in bin/* apps/*/*.sh lib/common.sh install.sh preinstall.sh postinstall.sh; do
|
||||||
|
bash -n "$f" || echo "FAIL: $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Initialize submodule after clone
|
||||||
|
git submodule update --init
|
||||||
|
|
||||||
|
# Pull latest ScaleTail services
|
||||||
|
git submodule update --remote compose/scale-tail
|
||||||
|
|
||||||
|
# List available compose services
|
||||||
|
./bin/wr-compose ls
|
||||||
|
|
||||||
|
# Test install in Docker
|
||||||
|
docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash
|
||||||
|
# inside container: cd /repo && ./install.sh
|
||||||
|
|
||||||
|
# Test app installation interactively
|
||||||
|
./apps/install.sh
|
||||||
|
./apps/install.sh --all # install all apps
|
||||||
|
./apps/install.sh docker vscode # install specific apps
|
||||||
|
```
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_brave() {
|
||||||
|
command -v brave-browser &>/dev/null && { log "brave already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Adding brave apt repo" bash -c "
|
||||||
|
sudo curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg \
|
||||||
|
https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg
|
||||||
|
echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main' \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/brave-browser-release.list >/dev/null
|
||||||
|
"
|
||||||
|
spawn "Installing brave-browser" sudo apt update -qq
|
||||||
|
spawn "Installing brave-browser" sudo apt install -y brave-browser
|
||||||
|
}
|
||||||
|
|
||||||
|
install_brave
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_opencode() {
|
||||||
|
command -v opencode &>/dev/null && { log "opencode already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing opencode" bash -c "
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
"
|
||||||
|
spawn "source bashrc" source ~/.bashrc
|
||||||
|
log "opencode installed to ~/.opencode/bin"
|
||||||
|
log "Add to PATH: export PATH=\"\$HOME/.opencode/bin:\$PATH\""
|
||||||
|
}
|
||||||
|
|
||||||
|
install_opencode
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_vscode() {
|
||||||
|
command -v code &>/dev/null && { log "vscode already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Adding vscode apt repo" bash -c "
|
||||||
|
sudo curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
|
||||||
|
| sudo gpg --dearmor -o /usr/share/keyrings/packages.microsoft.gpg
|
||||||
|
echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main' \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/vscode.list >/dev/null
|
||||||
|
"
|
||||||
|
spawn "Updating apt" sudo apt update -qq
|
||||||
|
spawn "Installing code" sudo apt install -y code
|
||||||
|
}
|
||||||
|
|
||||||
|
install_vscode
|
||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: bash apps/install.sh [OPTIONS] [app ...]
|
||||||
|
|
||||||
|
Install optional desktop applications.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--all Install all available apps without prompting
|
||||||
|
-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
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ALL=0
|
||||||
|
POSITIONAL=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--all) ALL=1; shift ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
-*) err "Unknown option: $1" ;;
|
||||||
|
*) POSITIONAL+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APPS_DIR="$(dirname "$0")"
|
||||||
|
|
||||||
|
# ── Category display names ─────────────────────────────────────
|
||||||
|
declare -A CAT_NAMES=(
|
||||||
|
[browsers]="Browsers"
|
||||||
|
[development]="Development"
|
||||||
|
[media]="Media"
|
||||||
|
[networking]="Networking / VPN"
|
||||||
|
[remote-access]="Remote Access"
|
||||||
|
[system]="System / Virtualization"
|
||||||
|
[utilities]="Utilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Discover categories and apps ───────────────────────────────
|
||||||
|
CATEGORIES=()
|
||||||
|
declare -A CAT_APPS
|
||||||
|
|
||||||
|
for cat_dir in "$APPS_DIR"/*/; do
|
||||||
|
[ -d "$cat_dir" ] || continue
|
||||||
|
cat_name=$(basename "$cat_dir")
|
||||||
|
# Skip non-app directories
|
||||||
|
[[ "$cat_name" == "install" ]] && continue
|
||||||
|
# Check if directory has any .sh files
|
||||||
|
has_apps=0
|
||||||
|
for f in "$cat_dir"*.sh; do
|
||||||
|
[ -f "$f" ] && has_apps=1 && break
|
||||||
|
done
|
||||||
|
[ "$has_apps" -eq 0 ] && continue
|
||||||
|
|
||||||
|
CATEGORIES+=("$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")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Helper: find which category an app belongs to ──────────────
|
||||||
|
find_app_category() {
|
||||||
|
local app="$1"
|
||||||
|
for cat in "${CATEGORIES[@]}"; do
|
||||||
|
for a in "${CAT_APPS[$cat]}"; do
|
||||||
|
[ "$a" == "$app" ] && { echo "$cat"; return 0; }
|
||||||
|
done
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
SELECTED=()
|
||||||
|
|
||||||
|
# ── Mode 1: specific apps requested on command line ────────────
|
||||||
|
if [ "${#POSITIONAL[@]}" -gt 0 ]; then
|
||||||
|
for req in "${POSITIONAL[@]}"; do
|
||||||
|
cat=$(find_app_category "$req" 2>/dev/null) && {
|
||||||
|
SELECTED+=("$req")
|
||||||
|
} || {
|
||||||
|
warn "Unknown app: $req (skipping)"
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Mode 2: --all ─────────────────────────────────────────────
|
||||||
|
elif [ "$ALL" -eq 1 ]; then
|
||||||
|
for cat in "${CATEGORIES[@]}"; do
|
||||||
|
for app in "${CAT_APPS[$cat]}"; do
|
||||||
|
SELECTED+=("$app")
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Mode 3: interactive TUI ───────────────────────────────────
|
||||||
|
else
|
||||||
|
section "Optional Applications"
|
||||||
|
echo "Select apps to install (y/n for each):"
|
||||||
|
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
|
||||||
|
if [[ "$yn" =~ ^[Yy] ]]; then
|
||||||
|
SELECTED+=("$app")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Install selected apps ──────────────────────────────────────
|
||||||
|
[ "${#SELECTED[@]}" -eq 0 ] && { warn "No apps selected"; exit 0; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
section "Installing ${SELECTED[*]}"
|
||||||
|
|
||||||
|
timer_start
|
||||||
|
count=1
|
||||||
|
total=${#SELECTED[@]}
|
||||||
|
for app in "${SELECTED[@]}"; do
|
||||||
|
cat=$(find_app_category "$app")
|
||||||
|
step "$count" "$total" "$app"
|
||||||
|
bash "$APPS_DIR/$cat/$app.sh"
|
||||||
|
count=$((count + 1))
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
|
echo "${GREEN} Apps installed ($(timer_stop))${RESET}"
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_obs() {
|
||||||
|
command -v obs &>/dev/null && { log "obs-studio already installed"; return 0; }
|
||||||
|
spawn "Installing obs-studio" sudo apt install -y obs-studio
|
||||||
|
}
|
||||||
|
|
||||||
|
install_obs
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
RELEASE_URL="https://api.github.com/repos/Genymobile/scrcpy/releases/latest"
|
||||||
|
|
||||||
|
install_scrcpy() {
|
||||||
|
command -v scrcpy &>/dev/null && { log "scrcpy already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Fetching latest scrcpy release info" bash -c "
|
||||||
|
curl -fsSL '$RELEASE_URL' -o /tmp/scrcpy-release.json
|
||||||
|
"
|
||||||
|
|
||||||
|
local tag asset_url
|
||||||
|
tag=$(python3 -c "import json; print(json.load(open('/tmp/scrcpy-release.json'))['tag_name'])")
|
||||||
|
asset_url=$(python3 -c "
|
||||||
|
import json
|
||||||
|
r = json.load(open('/tmp/scrcpy-release.json'))
|
||||||
|
for a in r['assets']:
|
||||||
|
if a['name'].startswith('scrcpy-linux-x86_64') and a['name'].endswith('.tar.gz'):
|
||||||
|
print(a['browser_download_url'])
|
||||||
|
break
|
||||||
|
")
|
||||||
|
version="${tag#v}"
|
||||||
|
|
||||||
|
spawn "Downloading scrcpy $version" bash -c "
|
||||||
|
install_dir=/usr/local/lib/scrcpy-$version
|
||||||
|
curl -fsSL '$asset_url' -o /tmp/scrcpy.tar.gz
|
||||||
|
sudo rm -rf \$install_dir /usr/local/lib/scrcpy
|
||||||
|
sudo mkdir -p \$install_dir
|
||||||
|
sudo tar xzf /tmp/scrcpy.tar.gz -C \$install_dir --strip-components=1
|
||||||
|
sudo ln -sf \$install_dir/scrcpy /usr/local/bin/scrcpy
|
||||||
|
rm -f /tmp/scrcpy.tar.gz /tmp/scrcpy-release.json
|
||||||
|
"
|
||||||
|
|
||||||
|
spawn "Adding desktop entry" bash -c "
|
||||||
|
sudo tee /usr/share/applications/scrcpy.desktop >/dev/null <<-EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=scrcpy
|
||||||
|
Comment=Display and control Android devices
|
||||||
|
Exec=/usr/local/bin/scrcpy
|
||||||
|
Icon=/usr/local/lib/scrcpy-$version/scrcpy.png
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;
|
||||||
|
StartupNotify=false
|
||||||
|
EOF
|
||||||
|
"
|
||||||
|
|
||||||
|
log "scrcpy $version installed (adb included in the bundle)"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_scrcpy
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_vlc() {
|
||||||
|
command -v vlc &>/dev/null && { log "vlc already installed"; return 0; }
|
||||||
|
spawn "Installing vlc" sudo apt install -y vlc
|
||||||
|
}
|
||||||
|
|
||||||
|
install_vlc
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_netbird() {
|
||||||
|
command -v netbird &>/dev/null && { log "netbird already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing netbird" bash -c "
|
||||||
|
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||||
|
"
|
||||||
|
log "Join a network: sudo netbird up --setup-key <key>"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_netbird
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_tailscale() {
|
||||||
|
command -v tailscale &>/dev/null && { log "tailscale already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing tailscale" bash -c "
|
||||||
|
curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
"
|
||||||
|
log "Start tailscale: sudo tailscale up"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_tailscale
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_zerotier() {
|
||||||
|
command -v zerotier-one &>/dev/null && { log "zerotier already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing zerotier" bash -c "
|
||||||
|
curl -s https://install.zerotier.com | sudo bash
|
||||||
|
"
|
||||||
|
log "Join a network: sudo zerotier-cli join <network-id>"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_zerotier
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_termius() {
|
||||||
|
command -v termius &>/dev/null && { log "termius already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Downloading Termius .deb" bash -c "
|
||||||
|
curl -fsSL -o /tmp/termius.deb 'https://www.termius.com/download/linux/Termius.deb'
|
||||||
|
"
|
||||||
|
|
||||||
|
spawn "Installing Termius" bash -c "
|
||||||
|
sudo dpkg -i /tmp/termius.deb || sudo apt-get install -f -y
|
||||||
|
rm -f /tmp/termius.deb
|
||||||
|
"
|
||||||
|
|
||||||
|
log "Termius installed — launch with 'termius'"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_termius
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_vnc_viewer() {
|
||||||
|
command -v vncviewer &>/dev/null && { log "tigervnc-viewer already installed"; return 0; }
|
||||||
|
spawn "Installing tigervnc-viewer" sudo apt install -y tigervnc-viewer
|
||||||
|
}
|
||||||
|
|
||||||
|
install_vnc_viewer
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_docker() {
|
||||||
|
command -v docker &>/dev/null && { log "docker already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing docker engine" bash -c "
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
"
|
||||||
|
spawn "Adding user to docker group" sudo usermod -aG docker "$USER"
|
||||||
|
warn "Log out and back in for docker group to take effect"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_qemu() {
|
||||||
|
command -v qemu-system-x86_64 &>/dev/null && { log "qemu already installed"; return 0; }
|
||||||
|
|
||||||
|
spawn "Installing qemu and libvirt" sudo apt install -y \
|
||||||
|
qemu-system qemu-utils qemu-kvm \
|
||||||
|
libvirt-daemon-system libvirt-clients \
|
||||||
|
bridge-utils virt-manager
|
||||||
|
|
||||||
|
spawn "Adding user to libvirt group" sudo usermod -aG libvirt "$USER"
|
||||||
|
spawn "Adding user to kvm group" sudo usermod -aG kvm "$USER"
|
||||||
|
warn "Log out and back in for libvirt/kvm groups to take effect"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_qemu
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_affine() {
|
||||||
|
command -v affine &>/dev/null && { log "affine already installed"; return 0; }
|
||||||
|
|
||||||
|
local appimage_url="https://github.com/toeverything/AFFiNE/releases/download/v0.26.3/affine-0.26.3-stable-linux-x64.appimage"
|
||||||
|
|
||||||
|
local icon_url="https://raw.githubusercontent.com/toeverything/AFFiNE/master/packages/frontend/apps/electron/resources/icons/icon.png"
|
||||||
|
|
||||||
|
spawn "Installing AFFiNE AppImage" bash -c "
|
||||||
|
mkdir -p /opt/affine
|
||||||
|
curl -fsSL '$appimage_url' -o /opt/affine/affine.AppImage
|
||||||
|
chmod +x /opt/affine/affine.AppImage
|
||||||
|
ln -sf /opt/affine/affine.AppImage /usr/local/bin/affine
|
||||||
|
"
|
||||||
|
|
||||||
|
spawn "Adding desktop entry" bash -c "
|
||||||
|
curl -fsSL '$icon_url' -o /opt/affine/icon.png
|
||||||
|
cat > /usr/share/applications/affine.desktop <<-EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=AFFiNE
|
||||||
|
Comment=Next-gen knowledge base
|
||||||
|
Exec=/opt/affine/affine.AppImage
|
||||||
|
Icon=/opt/affine/icon.png
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Office;Utility;
|
||||||
|
StartupNotify=false
|
||||||
|
EOF
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_affine
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_btop() {
|
||||||
|
command -v btop &>/dev/null && { log "btop already installed"; return 0; }
|
||||||
|
spawn "Installing btop" sudo apt install -y btop
|
||||||
|
}
|
||||||
|
|
||||||
|
install_btop
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../../lib/common.sh"
|
||||||
|
|
||||||
|
install_localsend() {
|
||||||
|
if flatpak list 2>/dev/null | grep -q org.localsend.localsend_app; then
|
||||||
|
log "localsend already installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v flatpak &>/dev/null; then
|
||||||
|
spawn "Installing flatpak" sudo apt install -y flatpak
|
||||||
|
fi
|
||||||
|
|
||||||
|
spawn "Adding flathub remote" sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
spawn "Installing localsend" sudo flatpak install -y flathub org.localsend.localsend_app
|
||||||
|
}
|
||||||
|
|
||||||
|
install_localsend
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
LOG="${HOME:-/root}/.autostart.log"
|
||||||
|
|
||||||
|
echo "[$(date)] autostart running" >> "$LOG" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Check network connectivity
|
||||||
|
if ping -c 1 -W 2 8.8.8.8 &>/dev/null; then
|
||||||
|
echo "[$(date)] Network: online" >> "$LOG" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "[$(date)] Network: offline" >> "$LOG" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[$(date)] autostart complete" >> "$LOG" 2>/dev/null || true
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
self="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# ── Colors (auto-off when not a TTY) ───────────────────────────
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
BOLD=$(tput bold 2>/dev/null || true)
|
||||||
|
CYAN=$(tput setaf 6 2>/dev/null || true)
|
||||||
|
DIM=$(tput dim 2>/dev/null || true)
|
||||||
|
RESET=$(tput sgr0 2>/dev/null || true)
|
||||||
|
else
|
||||||
|
BOLD=""; CYAN=""; DIM=""; RESET=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Collect available commands ─────────────────────────────────
|
||||||
|
_pos_commands() {
|
||||||
|
local cmds=()
|
||||||
|
local f
|
||||||
|
for f in "$self"/pos-*; do
|
||||||
|
[ -x "$f" ] || continue
|
||||||
|
local name="${f##*/pos-}"
|
||||||
|
cmds+=("$name")
|
||||||
|
done
|
||||||
|
echo "${cmds[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Help text ──────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
pos — Personal OS Toolkit
|
||||||
|
|
||||||
|
A unified CLI for network, docker, media, system,
|
||||||
|
and SSH tools on Debian/Ubuntu.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 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 media mp3 <url> Download audio as MP3
|
||||||
|
pos media mp4 <url> Download video as MP4
|
||||||
|
|
||||||
|
pos system firewall Interactive UFW manager
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
HELP
|
||||||
|
pos help <command> Show help for a command
|
||||||
|
pos --help Show this help
|
||||||
|
|
||||||
|
LEGACY WRAPPERS
|
||||||
|
wr-ip, wr-checkport, wr-scan-ping, wr-docker,
|
||||||
|
wr-compose, wr-ufw, mp3, mp4, vbox, ssh-load-all
|
||||||
|
still work and forward to pos.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Handle --help / -h ─────────────────────────────────────────
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── pos help <command> ─────────────────────────────────────────
|
||||||
|
if [ "${1:-}" = "help" ]; then
|
||||||
|
shift
|
||||||
|
[ $# -eq 0 ] && usage
|
||||||
|
cmd="pos-${1// /-}"
|
||||||
|
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 "Run 'pos --help' to see available commands." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Dispatch ───────────────────────────────────────────────────
|
||||||
|
args=("$@")
|
||||||
|
n=${#args[@]}
|
||||||
|
|
||||||
|
# ── Logging setup ──────────────────────────────────────────────
|
||||||
|
LOG_DIR="$HOME/.local/share/mylinux/logs"
|
||||||
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
||||||
|
CMD_SAFE=$(echo "${args[*]}" | tr ' /' '__')
|
||||||
|
LOG_FILE="$LOG_DIR/$(date +%Y%m%d_%H%M%S)_pos_${CMD_SAFE}.log"
|
||||||
|
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"
|
||||||
|
|
||||||
|
for ((i=n-1; i>=0; i--)); do
|
||||||
|
cmd="pos"
|
||||||
|
for ((j=0; j<=i; j++)); do
|
||||||
|
cmd="${cmd}-${args[$j]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
resolved=""
|
||||||
|
if command -v "$cmd" &>/dev/null; then
|
||||||
|
resolved="$cmd"
|
||||||
|
elif [ -x "$self/$cmd" ]; then
|
||||||
|
resolved="$self/$cmd"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z "$resolved" ] && continue
|
||||||
|
|
||||||
|
sub="${cmd#pos-}"
|
||||||
|
if [[ " $INTERACTIVE_CMDS " == *" $sub "* ]]; then
|
||||||
|
# Interactive: log invocation only, then exec normally
|
||||||
|
log_cmd "pos $*" "" 0
|
||||||
|
exec "$resolved" "${args[@]:i+1}"
|
||||||
|
else
|
||||||
|
# Non-interactive: capture full output
|
||||||
|
"$resolved" "${args[@]:i+1}" 2>&1 | tee "$LOG_FILE"
|
||||||
|
rc=${PIPESTATUS[0]}
|
||||||
|
log_cmd "pos $*" "$LOG_FILE" "$rc"
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "pos: unknown command '${args[0]}'" >&2
|
||||||
|
echo "Run 'pos --help' to see available commands." >&2
|
||||||
|
exit 1
|
||||||
Executable
+317
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
SCALE_DIR="/usr/local/share/mylinux/scale-tail/services"
|
||||||
|
CONFIG_ENV="${HOME}/.config/mylinux/compose.env"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage:
|
||||||
|
pos docker compose ls List available ScaleTail services
|
||||||
|
pos docker compose installed List deployed services
|
||||||
|
pos docker compose up <service> Deploy a service
|
||||||
|
pos docker compose down <service> Stop a service
|
||||||
|
pos docker compose restart <service> Restart a service
|
||||||
|
pos docker compose logs <service> [-f] View service logs
|
||||||
|
pos docker compose update Pull latest ScaleTail + refresh compose files
|
||||||
|
pos docker compose config Show global config
|
||||||
|
pos docker compose config set KEY=VALUE Set a global config value
|
||||||
|
pos docker compose config edit Open global config in editor
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos docker compose up jellyfin
|
||||||
|
pos docker compose down actual-budget
|
||||||
|
pos docker compose logs home-assistant -f
|
||||||
|
pos docker compose config set TS_AUTHKEY=tskey-auth-xxxxx
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_deps() {
|
||||||
|
command -v docker &>/dev/null || err "docker not found — run 'install.sh --apps' and install Docker first"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_templates() {
|
||||||
|
[ -d "$SCALE_DIR" ] || err "ScaleTail templates not found at $SCALE_DIR — run 'install.sh' to set them up"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_service() {
|
||||||
|
local svc="$1"
|
||||||
|
[ -d "$SCALE_DIR/$svc" ] || err "Unknown service '$svc' — run 'pos docker compose ls' to see available services"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_global_config() {
|
||||||
|
mkdir -p "$(dirname "$CONFIG_ENV")"
|
||||||
|
[ -f "$CONFIG_ENV" ] && source "$CONFIG_ENV"
|
||||||
|
SERVICES_BASE="${SERVICES_BASE:-/srv}"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_dir() {
|
||||||
|
local svc="$1"
|
||||||
|
echo "$SERVICES_BASE/$svc"
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Commands
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
cmd_ls() {
|
||||||
|
check_templates
|
||||||
|
local names=()
|
||||||
|
for svc in "$SCALE_DIR"/*/; do
|
||||||
|
names+=("$(basename "$svc")")
|
||||||
|
done
|
||||||
|
IFS=$'\n' names=($(sort <<<"${names[*]}")); unset IFS
|
||||||
|
|
||||||
|
echo "${CYAN}Available ScaleTail services:${RESET}"
|
||||||
|
echo "${BLUE}────────────────────────────────────────${RESET}"
|
||||||
|
local name
|
||||||
|
for name in "${names[@]}"; do
|
||||||
|
printf " ${GREEN}%s${RESET}\n" "$name"
|
||||||
|
done
|
||||||
|
echo "${BLUE}────────────────────────────────────────${RESET}"
|
||||||
|
echo " ${#names[@]} services total"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_installed() {
|
||||||
|
load_global_config
|
||||||
|
local count=0
|
||||||
|
|
||||||
|
echo "${CYAN}Deployed services:${RESET}"
|
||||||
|
echo "${BLUE}────────────────────────────────────────${RESET}"
|
||||||
|
|
||||||
|
[ -d "$SERVICES_BASE" ] || { echo " (none — $SERVICES_BASE does not exist)"; echo "${BLUE}────────────────────────────────────────${RESET}"; echo " 0 services deployed"; return; }
|
||||||
|
|
||||||
|
for svc in "$SERVICES_BASE"/*/; do
|
||||||
|
[ -d "$svc" ] || continue
|
||||||
|
local name
|
||||||
|
name=$(basename "$svc")
|
||||||
|
local status=""
|
||||||
|
if [ -f "$svc/compose.yaml" ] || [ -f "$svc/compose.yml" ]; then
|
||||||
|
status=$(docker compose ls --format json 2>/dev/null | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
data = [data]
|
||||||
|
for e in data:
|
||||||
|
if e.get('Name') == '$name':
|
||||||
|
print(e.get('Status', 'unknown'))
|
||||||
|
break
|
||||||
|
except: pass
|
||||||
|
" 2>/dev/null || echo "unknown")
|
||||||
|
fi
|
||||||
|
printf " ${GREEN}%-28s${RESET} %s\n" "$name" "${status:-unknown}"
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
echo "${BLUE}────────────────────────────────────────${RESET}"
|
||||||
|
echo " $count services deployed"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_up() {
|
||||||
|
local svc="$1"
|
||||||
|
[ -z "$svc" ] && usage
|
||||||
|
check_deps
|
||||||
|
check_templates
|
||||||
|
check_service "$svc"
|
||||||
|
load_global_config
|
||||||
|
|
||||||
|
local target
|
||||||
|
target=$(service_dir "$svc")
|
||||||
|
|
||||||
|
if [ ! -d "$target" ]; then
|
||||||
|
log "Creating $svc at $target"
|
||||||
|
mkdir -p "$target/config" "$target/data"
|
||||||
|
if [ -f "$SCALE_DIR/$svc/compose.yaml" ]; then
|
||||||
|
cp "$SCALE_DIR/$svc/compose.yaml" "$target/"
|
||||||
|
fi
|
||||||
|
if [ -f "$SCALE_DIR/$svc/compose.yml" ]; then
|
||||||
|
cp "$SCALE_DIR/$svc/compose.yml" "$target/"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
mkdir -p "$target/config" "$target/data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local env_file="$target/.env"
|
||||||
|
if [ ! -f "$env_file" ]; then
|
||||||
|
if [ -f "$SCALE_DIR/$svc/.env" ]; then
|
||||||
|
cp "$SCALE_DIR/$svc/.env" "$env_file"
|
||||||
|
else
|
||||||
|
cat > "$env_file" <<-EOF
|
||||||
|
SERVICE=$svc
|
||||||
|
IMAGE_URL=
|
||||||
|
SERVICEPORT=
|
||||||
|
DNS_SERVER=${DNS_SERVER:-9.9.9.9}
|
||||||
|
TS_AUTHKEY=${TS_AUTHKEY:-}
|
||||||
|
TZ=${TZ:-Europe/Amsterdam}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${TS_AUTHKEY:-}" ]; then
|
||||||
|
sed -i "s|^TS_AUTHKEY=.*|TS_AUTHKEY=$TS_AUTHKEY|" "$env_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -n "${TZ:-}" ]; then
|
||||||
|
sed -i "s|^TZ=.*|TZ=$TZ|" "$env_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -n "${DNS_SERVER:-}" ]; then
|
||||||
|
sed -i "s|^DNS_SERVER=.*|DNS_SERVER=$DNS_SERVER|" "$env_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "^TS_AUTHKEY=$" "$env_file" 2>/dev/null; then
|
||||||
|
warn "TS_AUTHKEY is not set"
|
||||||
|
local key
|
||||||
|
read -rp " Enter your Tailscale auth key (or press Enter to skip): " key
|
||||||
|
if [ -n "$key" ]; then
|
||||||
|
sed -i "s|^TS_AUTHKEY=.*|TS_AUTHKEY=$key|" "$env_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
log ".env created at $env_file — edit it if needed before continuing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting $svc..."
|
||||||
|
(cd "$target" && docker compose up -d)
|
||||||
|
ok "$svc is running"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_down() {
|
||||||
|
local svc="$1"
|
||||||
|
[ -z "$svc" ] && usage
|
||||||
|
check_deps
|
||||||
|
load_global_config
|
||||||
|
local target
|
||||||
|
target=$(service_dir "$svc")
|
||||||
|
[ -d "$target" ] || err "$svc is not deployed at $target"
|
||||||
|
log "Stopping $svc..."
|
||||||
|
(cd "$target" && docker compose down)
|
||||||
|
ok "$svc stopped"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
local svc="$1"
|
||||||
|
[ -z "$svc" ] && usage
|
||||||
|
check_deps
|
||||||
|
load_global_config
|
||||||
|
local target
|
||||||
|
target=$(service_dir "$svc")
|
||||||
|
[ -d "$target" ] || err "$svc is not deployed"
|
||||||
|
log "Restarting $svc..."
|
||||||
|
(cd "$target" && docker compose restart)
|
||||||
|
ok "$svc restarted"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() {
|
||||||
|
local svc="$1"
|
||||||
|
shift 2>/dev/null || true
|
||||||
|
[ -z "$svc" ] && usage
|
||||||
|
check_deps
|
||||||
|
load_global_config
|
||||||
|
local target
|
||||||
|
target=$(service_dir "$svc")
|
||||||
|
[ -d "$target" ] || err "$svc is not deployed"
|
||||||
|
(cd "$target" && exec docker compose logs "$@")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_update() {
|
||||||
|
check_templates
|
||||||
|
|
||||||
|
log "Updating ScaleTail templates..."
|
||||||
|
sudo git -C "/usr/local/share/mylinux/scale-tail" pull
|
||||||
|
ok "Templates updated"
|
||||||
|
|
||||||
|
load_global_config
|
||||||
|
local refreshed=0
|
||||||
|
|
||||||
|
[ -d "$SERVICES_BASE" ] || { ok "$refreshed compose files refreshed (.env preserved)"; return; }
|
||||||
|
|
||||||
|
for svc in "$SERVICES_BASE"/*/; do
|
||||||
|
[ -d "$svc" ] || continue
|
||||||
|
local name
|
||||||
|
name=$(basename "$svc")
|
||||||
|
if [ -f "$SCALE_DIR/$name/compose.yaml" ]; then
|
||||||
|
cp "$SCALE_DIR/$name/compose.yaml" "$svc/compose.yaml"
|
||||||
|
refreshed=$((refreshed + 1))
|
||||||
|
fi
|
||||||
|
if [ -f "$SCALE_DIR/$name/compose.yml" ]; then
|
||||||
|
cp "$SCALE_DIR/$name/compose.yml" "$svc/compose.yml"
|
||||||
|
refreshed=$((refreshed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ok "$refreshed compose files refreshed (.env preserved)"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_config() {
|
||||||
|
local action="${1:-show}"
|
||||||
|
shift 2>/dev/null || true
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
show)
|
||||||
|
load_global_config
|
||||||
|
if [ -f "$CONFIG_ENV" ]; then
|
||||||
|
echo "${CYAN}Global compose config:${RESET}"
|
||||||
|
echo "${BLUE}────────────────────────────────────────${RESET}"
|
||||||
|
cat "$CONFIG_ENV"
|
||||||
|
echo "${BLUE}────────────────────────────────────────${RESET}"
|
||||||
|
echo "SERVICES_BASE=$SERVICES_BASE"
|
||||||
|
else
|
||||||
|
warn "No config file found at $CONFIG_ENV"
|
||||||
|
echo "Default SERVICES_BASE=/srv"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
set)
|
||||||
|
local pair="${1:-}"
|
||||||
|
[ -z "$pair" ] && usage
|
||||||
|
local key="${pair%%=*}"
|
||||||
|
local val="${pair#*=}"
|
||||||
|
mkdir -p "$(dirname "$CONFIG_ENV")"
|
||||||
|
if [ -f "$CONFIG_ENV" ] && grep -q "^${key}=" "$CONFIG_ENV" 2>/dev/null; then
|
||||||
|
sed -i "s|^${key}=.*|${key}=${val}|" "$CONFIG_ENV"
|
||||||
|
else
|
||||||
|
echo "${key}=${val}" >> "$CONFIG_ENV"
|
||||||
|
fi
|
||||||
|
ok "Set ${key}=${val}"
|
||||||
|
;;
|
||||||
|
edit)
|
||||||
|
mkdir -p "$(dirname "$CONFIG_ENV")"
|
||||||
|
if [ ! -f "$CONFIG_ENV" ]; then
|
||||||
|
cat > "$CONFIG_ENV" <<-EOF
|
||||||
|
TS_AUTHKEY=
|
||||||
|
TZ=Europe/Amsterdam
|
||||||
|
DNS_SERVER=9.9.9.9
|
||||||
|
SERVICES_BASE=/srv
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
"${EDITOR:-nano}" "$CONFIG_ENV"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# CLI dispatch
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
[ $# -eq 0 ] && usage
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
ls) cmd_ls ;;
|
||||||
|
installed) cmd_installed ;;
|
||||||
|
up) cmd_up "${1:-}" ;;
|
||||||
|
down) cmd_down "${1:-}" ;;
|
||||||
|
restart) cmd_restart "${1:-}" ;;
|
||||||
|
logs) cmd_logs "$@" ;;
|
||||||
|
update) cmd_update ;;
|
||||||
|
config) cmd_config "$@" ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos docker health
|
||||||
|
|
||||||
|
Quick one-glance health dashboard for all Docker containers.
|
||||||
|
Shows health status, uptime, and exits with code 1 if any container is unhealthy.
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 All containers healthy (or no healthcheck configured)
|
||||||
|
1 One or more containers unhealthy
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo "docker not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! container_ids=$(docker ps -a -q 2>/dev/null) || [[ -z "$container_ids" ]]; then
|
||||||
|
echo "No containers."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
healthy=0
|
||||||
|
unhealthy=0
|
||||||
|
no_check=0
|
||||||
|
other=0
|
||||||
|
|
||||||
|
echo "── Docker Health ─────────────────────────────────"
|
||||||
|
|
||||||
|
for cid in $container_ids; do
|
||||||
|
info=$(docker inspect "$cid" 2>/dev/null) || continue
|
||||||
|
|
||||||
|
name=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Name'].lstrip('/'))" 2>/dev/null)
|
||||||
|
health=$(echo "$info" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
h=d.get('State',{}).get('Health',{})
|
||||||
|
print(h.get('Status','') if h else '')
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
started_at=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['State']['StartedAt'])" 2>/dev/null)
|
||||||
|
status=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])" 2>/dev/null)
|
||||||
|
|
||||||
|
# Calculate uptime
|
||||||
|
uptime="-"
|
||||||
|
if start_ts=$(date -d "$started_at" +%s 2>/dev/null); then
|
||||||
|
now_ts=$(date +%s)
|
||||||
|
diff=$((now_ts - start_ts))
|
||||||
|
days=$((diff / 86400))
|
||||||
|
hours=$(((diff % 86400) / 3600))
|
||||||
|
mins=$(((diff % 3600) / 60))
|
||||||
|
if [ "$days" -gt 0 ]; then
|
||||||
|
uptime="${days}d ${hours}h"
|
||||||
|
elif [ "$hours" -gt 0 ]; then
|
||||||
|
uptime="${hours}h ${mins}m"
|
||||||
|
else
|
||||||
|
uptime="${mins}m"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine health display
|
||||||
|
if [ -n "$health" ]; then
|
||||||
|
case "$health" in
|
||||||
|
healthy)
|
||||||
|
health_display="${GREEN}healthy${RESET}"
|
||||||
|
healthy=$((healthy + 1))
|
||||||
|
;;
|
||||||
|
unhealthy)
|
||||||
|
health_display="${RED}unhealthy${RESET}"
|
||||||
|
unhealthy=$((unhealthy + 1))
|
||||||
|
;;
|
||||||
|
starting)
|
||||||
|
health_display="${YELLOW}starting${RESET}"
|
||||||
|
other=$((other + 1))
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
health_display="${YELLOW}${health}${RESET}"
|
||||||
|
other=$((other + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
elif [ "$status" != "running" ]; then
|
||||||
|
health_display="${RED}${status}${RESET}"
|
||||||
|
other=$((other + 1))
|
||||||
|
else
|
||||||
|
health_display="${YELLOW}no healthcheck${RESET}"
|
||||||
|
no_check=$((no_check + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " %-24s %b%-20s${RESET} %s\n" "$name" "" "$health_display" "$uptime"
|
||||||
|
done
|
||||||
|
|
||||||
|
total=$((healthy + unhealthy + no_check + other))
|
||||||
|
echo "──────────────────────────────────────────────────"
|
||||||
|
echo " Total: $total ${GREEN}healthy: $healthy${RESET} ${RED}unhealthy: $unhealthy${RESET} no check: $no_check"
|
||||||
|
|
||||||
|
if [ "$unhealthy" -gt 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "${RED}Unhealthy containers detected${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos docker ps
|
||||||
|
|
||||||
|
Display enhanced Docker container overview with health status,
|
||||||
|
uptime, IPs, and port mappings.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
echo "docker not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! container_ids=$(docker ps -q 2>/dev/null) || [[ -z "$container_ids" ]]; then
|
||||||
|
echo "No running containers."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
healthy_count=0
|
||||||
|
unhealthy_count=0
|
||||||
|
running_count=0
|
||||||
|
total_count=0
|
||||||
|
|
||||||
|
printf "%-28s %-35s %-22s %-10s %-35s %-20s %s\n" \
|
||||||
|
"NAME" "IMAGE" "STATUS(HEALTH)" "UPTIME" "IPS" "PORTS" "CONTAINER ID"
|
||||||
|
|
||||||
|
for cid in $container_ids; do
|
||||||
|
total_count=$((total_count + 1))
|
||||||
|
|
||||||
|
info=$(docker inspect "$cid" 2>/dev/null) || continue
|
||||||
|
|
||||||
|
name=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Name'].lstrip('/'))" 2>/dev/null)
|
||||||
|
image=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['Config']['Image'])" 2>/dev/null)
|
||||||
|
status=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['State']['Status'])" 2>/dev/null)
|
||||||
|
health=$(echo "$info" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
h=d.get('State',{}).get('Health',{})
|
||||||
|
print(h.get('Status','') if h else '')
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
started_at=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['State']['StartedAt'])" 2>/dev/null)
|
||||||
|
ip_data=$(echo "$info" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
nets=d.get('NetworkSettings',{}).get('Networks',{})
|
||||||
|
out=' '.join(f'{k}:{v.get(\"IPAddress\",\"\")}' for k,v in nets.items() if v.get('IPAddress'))
|
||||||
|
print(out or '-')
|
||||||
|
" 2>/dev/null)
|
||||||
|
port_data=$(echo "$info" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
ports=d.get('NetworkSettings',{}).get('Ports',{}) or {}
|
||||||
|
parts=[]
|
||||||
|
for container_port, bindings in ports.items():
|
||||||
|
if bindings:
|
||||||
|
for b in bindings:
|
||||||
|
hp=b.get('HostPort','')
|
||||||
|
hi=b.get('HostIp','')
|
||||||
|
if hi and hp:
|
||||||
|
parts.append(f'{hi}:{hp}->{container_port}')
|
||||||
|
elif hp:
|
||||||
|
parts.append(f'{hp}->{container_port}')
|
||||||
|
else:
|
||||||
|
parts.append(container_port)
|
||||||
|
else:
|
||||||
|
parts.append(container_port)
|
||||||
|
print(', '.join(parts) if parts else '-')
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
[ "${#image}" -gt 35 ] && image="${image:0:32}..."
|
||||||
|
|
||||||
|
if [ "$status" = "running" ]; then
|
||||||
|
running_count=$((running_count + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$health" ]; then
|
||||||
|
case "$health" in
|
||||||
|
healthy) healthy_count=$((healthy_count + 1)); status_display="${GREEN}${status} (${health})${RESET}" ;;
|
||||||
|
unhealthy) unhealthy_count=$((unhealthy_count + 1)); status_display="${RED}${status} (${health})${RESET}" ;;
|
||||||
|
*) status_display="${YELLOW}${status} (${health})${RESET}" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
status_display="${YELLOW}${status}${RESET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
uptime="-"
|
||||||
|
if start_ts=$(date -d "$started_at" +%s 2>/dev/null); then
|
||||||
|
now_ts=$(date +%s)
|
||||||
|
diff=$((now_ts - start_ts))
|
||||||
|
days=$((diff / 86400))
|
||||||
|
hours=$(((diff % 86400) / 3600))
|
||||||
|
mins=$(((diff % 3600) / 60))
|
||||||
|
if [ "$days" -gt 0 ]; then
|
||||||
|
uptime="${days}d ${hours}h"
|
||||||
|
elif [ "$hours" -gt 0 ]; then
|
||||||
|
uptime="${hours}h ${mins}m"
|
||||||
|
else
|
||||||
|
uptime="${mins}m"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%-28s %-35s %-22b %-10s %-35s %-20s %s\n" \
|
||||||
|
"$name" "$image" "$status_display" "$uptime" "$ip_data" "$port_data" "$cid"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
printf '%*s\n' 120 '' | tr ' ' '-'
|
||||||
|
|
||||||
|
echo "Containers : $total_count"
|
||||||
|
echo "Healthy : $healthy_count"
|
||||||
|
echo "Unhealthy : $unhealthy_count"
|
||||||
|
echo "Running : $running_count"
|
||||||
|
|
||||||
|
if [ "$unhealthy_count" -gt 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "${RED}Warning: unhealthy containers detected${RESET}"
|
||||||
|
fi
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos media mp3 <url>
|
||||||
|
|
||||||
|
Download audio from a URL and convert to MP3 via yt-dlp.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos media mp3 https://youtube.com/watch?v=dQw4w9WgXcQ
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
url="$1"
|
||||||
|
|
||||||
|
yt-dlp \
|
||||||
|
-x \
|
||||||
|
--audio-format mp3 \
|
||||||
|
--audio-quality 0 \
|
||||||
|
--embed-thumbnail \
|
||||||
|
--convert-thumbnails jpg \
|
||||||
|
--add-metadata \
|
||||||
|
-o "$HOME/Music/%(title)s.%(ext)s" \
|
||||||
|
"$url"
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos media mp4 <url>
|
||||||
|
|
||||||
|
Download video from a URL via yt-dlp with interactive format selection.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos media mp4 https://youtube.com/watch?v=dQw4w9WgXcQ
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
url="$1"
|
||||||
|
|
||||||
|
yt-dlp -F "$url"
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -rp "Enter format ID: " format
|
||||||
|
|
||||||
|
yt-dlp \
|
||||||
|
-f "$format" \
|
||||||
|
--merge-output-format mp4 \
|
||||||
|
--embed-thumbnail \
|
||||||
|
--add-metadata \
|
||||||
|
-o "$HOME/Videos/%(title)s.%(ext)s" \
|
||||||
|
"$url"
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos network checkport <ip:port>
|
||||||
|
|
||||||
|
Check if a TCP port is open on a remote host.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos network checkport 192.168.1.1:80
|
||||||
|
pos network checkport 10.0.0.5:443
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
target="$1"
|
||||||
|
|
||||||
|
if [[ "$target" != *:* ]]; then
|
||||||
|
echo "ERROR: Expected <ip:port> format, got '$target'"
|
||||||
|
echo "Usage: pos network checkport <ip:port>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ip="${target%:*}"
|
||||||
|
port="${target#*:}"
|
||||||
|
|
||||||
|
if [[ -z "$ip" || -z "$port" ]]; then
|
||||||
|
echo "ERROR: Invalid target '$target'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking $ip:$port ..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
if timeout 2 bash -c "cat < /dev/null > /dev/tcp/$ip/$port" 2>/dev/null; then
|
||||||
|
echo "OPEN ✔ $ip:$port"
|
||||||
|
else
|
||||||
|
echo "CLOSED ✖ $ip:$port"
|
||||||
|
fi
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos network ip
|
||||||
|
|
||||||
|
Display network interfaces, default route, and public IP.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
SEP() { printf '\u2550%.0s' $(seq 1 47); echo; }
|
||||||
|
|
||||||
|
SEP
|
||||||
|
echo "Network Interfaces"
|
||||||
|
SEP
|
||||||
|
|
||||||
|
ip -o -4 addr show | while read -r _ ifname _ ipaddr _; do
|
||||||
|
printf "%-20s %s\n" "$ifname" "${ipaddr%%/*}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
SEP
|
||||||
|
echo "Default Route"
|
||||||
|
SEP
|
||||||
|
|
||||||
|
gateway=$(ip route | awk '/default/ {print $3; exit}')
|
||||||
|
iface=$(ip route | awk '/default/ {print $5; exit}')
|
||||||
|
|
||||||
|
printf "%-20s %s\n" "Interface" "${iface:-N/A}"
|
||||||
|
printf "%-20s %s\n" "Gateway" "${gateway:-N/A}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
|
||||||
|
SEP
|
||||||
|
echo "Public IP"
|
||||||
|
SEP
|
||||||
|
|
||||||
|
curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || echo "Unavailable"
|
||||||
|
echo
|
||||||
Executable
+270
@@ -0,0 +1,270 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos network scan <cidr> [--full] [--retries N]
|
||||||
|
|
||||||
|
Two-phase network scan using nmap.
|
||||||
|
|
||||||
|
Phase 1: Fast host discovery (finds alive hosts)
|
||||||
|
Phase 2: Full metadata scan on alive hosts only (--full only)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--full Detailed scan: OS, ports, services, NSE scripts (slower)
|
||||||
|
--retries N Retries per host in discovery (default: 1)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
pos network scan 192.168.1.0/24
|
||||||
|
pos network scan 10.0.0.0/28 --full
|
||||||
|
pos network scan 172.1.1.104
|
||||||
|
pos network scan 192.168.1.0/24 --retries 3
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help|"") usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
net=""
|
||||||
|
full=0
|
||||||
|
retries=1
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--full) full=1; shift ;;
|
||||||
|
--retries)
|
||||||
|
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
||||||
|
echo "ERROR: --retries requires a number"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
retries="$2"; shift 2 ;;
|
||||||
|
*) net="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$net" ]]; then
|
||||||
|
echo "ERROR: Missing CIDR (e.g. 192.168.1.0/24)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Input validation ───────────────────────────────────────────
|
||||||
|
# Bare IP → /32
|
||||||
|
if [[ "$net" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||||
|
net="${net}/32"
|
||||||
|
# Valid CIDR
|
||||||
|
elif [[ "$net" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
|
||||||
|
: # ok
|
||||||
|
else
|
||||||
|
echo "ERROR: Invalid target '$net'"
|
||||||
|
echo "Expected: IP (172.1.1.104) or CIDR (192.168.1.0/24)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v nmap &>/dev/null; then
|
||||||
|
echo "ERROR: nmap is required. Install with: sudo apt install nmap"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Estimate host count ────────────────────────────────────────
|
||||||
|
cidr_bits="${net##*/}"
|
||||||
|
if [[ "$cidr_bits" -ge 24 ]]; then
|
||||||
|
host_estimate=$(( 1 << (32 - cidr_bits) ))
|
||||||
|
elif [[ "$cidr_bits" -ge 16 ]]; then
|
||||||
|
host_estimate="$(( 1 << (32 - cidr_bits) ))+"
|
||||||
|
else
|
||||||
|
host_estimate="many"
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_root=0
|
||||||
|
[[ $EUID -eq 0 ]] && is_root=1
|
||||||
|
|
||||||
|
can_sudo=0
|
||||||
|
if [[ "$is_root" -eq 1 ]]; then
|
||||||
|
can_sudo=1
|
||||||
|
elif sudo -n nmap -V &>/dev/null; then
|
||||||
|
can_sudo=1
|
||||||
|
elif [[ "$full" -eq 1 && -t 0 ]]; then
|
||||||
|
can_sudo=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmpfile=$(mktemp /tmp/scan-XXXXXX.txt)
|
||||||
|
trap 'rm -f "$tmpfile"' EXIT
|
||||||
|
|
||||||
|
# ── Phase 1: Fast host discovery ───────────────────────────────
|
||||||
|
nmap_cmd="nmap"
|
||||||
|
[[ "$can_sudo" -eq 1 ]] && nmap_cmd="sudo nmap"
|
||||||
|
|
||||||
|
echo "Discovering hosts in $net (~$host_estimate) ..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
$nmap_cmd -sn -T5 -n \
|
||||||
|
--min-rate 1000 \
|
||||||
|
--min-parallelism 1024 \
|
||||||
|
--min-hostgroup 1024 \
|
||||||
|
--max-retries "$retries" \
|
||||||
|
--host-timeout 5s \
|
||||||
|
"$net" 2>/dev/null | awk '/^Nmap scan report for/ {
|
||||||
|
ip = $(NF);
|
||||||
|
gsub(/[()]/, "", ip);
|
||||||
|
print ip;
|
||||||
|
}' > "$tmpfile"
|
||||||
|
|
||||||
|
host_count=$(wc -l < "$tmpfile")
|
||||||
|
|
||||||
|
if [[ "$host_count" -eq 0 ]]; then
|
||||||
|
echo "No hosts found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $host_count host(s)."
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ "$full" -eq 0 ]]; then
|
||||||
|
cat "$tmpfile"
|
||||||
|
echo
|
||||||
|
echo "Done."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 2: Full metadata scan ────────────────────────────────
|
||||||
|
echo "Scanning $host_count host(s) (full) ..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
nmap_args="-sV --version-intensity 9 -sC -T4 -n"
|
||||||
|
nmap_args="$nmap_args --min-parallelism 256 --max-parallelism 512"
|
||||||
|
nmap_args="$nmap_args --min-hostgroup 256 --max-hostgroup 512"
|
||||||
|
nmap_args="$nmap_args --min-rate 1000 --max-retries 1"
|
||||||
|
nmap_args="$nmap_args --host-timeout 60s --max-rtt-timeout 200ms"
|
||||||
|
nmap_args="$nmap_args --script ssh-hostkey,ssl-cert,http-title,http-server-header,smb-os-discovery,nbstat,rpcinfo"
|
||||||
|
[[ "$can_sudo" -eq 1 ]] && nmap_args="$nmap_args -O --osscan-guess"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
$nmap_cmd $nmap_args -iL "$tmpfile" 2>/dev/null | awk '
|
||||||
|
BEGIN { ip_count = 0; has_os = 0 }
|
||||||
|
|
||||||
|
/^Nmap scan report for/ {
|
||||||
|
ip = $(NF);
|
||||||
|
gsub(/[()]/, "", ip);
|
||||||
|
hostname = "";
|
||||||
|
if ($(NF) ~ /^\(/) {
|
||||||
|
hostname = $(NF-1);
|
||||||
|
} else if (NF > 4) {
|
||||||
|
if (ip != $(NF-1) && $(NF-1) !~ /^(for|[0-9])/) {
|
||||||
|
hostname = $(NF-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ip_count > 0) printf "\n";
|
||||||
|
ip_count++;
|
||||||
|
has_os = 0;
|
||||||
|
if (hostname != "" && hostname != ip)
|
||||||
|
printf "\033[1;36m%s\033[0m (%s)\n", ip, hostname;
|
||||||
|
else
|
||||||
|
printf "\033[1;36m%s\033[0m\n", ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^MAC Address/ {
|
||||||
|
vendor = $0;
|
||||||
|
sub(/.*\(/, "", vendor);
|
||||||
|
sub(/\).*/, "", vendor);
|
||||||
|
printf " \033[2m%-10s\033[0m %s %s\n", "MAC:", $3, vendor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^Aggressive OS guesses:/ {
|
||||||
|
has_os = 1;
|
||||||
|
line = $0;
|
||||||
|
sub(/.*guesses: /, "", line);
|
||||||
|
gsub(/\s*\(.*/, "", line);
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "OS:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^OS details:/ {
|
||||||
|
has_os = 1;
|
||||||
|
sub(/.*OS details: /, "");
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "OS:", $0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^Running:/ {
|
||||||
|
sub(/.*Running: /, "");
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "OS:", $0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^Service Info:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*Service Info:/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
if (has_os == 0)
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "Info:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\| ssh-hostkey:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*ssh-hostkey:/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
if (line ~ /SHA256/) {
|
||||||
|
match(line, /SHA256:[A-Za-z0-9+\/=]+/);
|
||||||
|
key = substr(line, RSTART, RLENGTH);
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "SSH:", key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\| http-title:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*http-title:/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
gsub(/\s*\[.*$/, "", line);
|
||||||
|
if (line != "" && line !~ /^No/)
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "HTTP Title:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\| http-server-header:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*http-server-header:/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "HTTP Server:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\|_?NetBIOS name:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*NetBIOS name:/, "", line);
|
||||||
|
sub(/,.*$/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "NetBIOS:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\|_?SMB OS:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*SMB OS:/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "SMB:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\|_?Domain:/ {
|
||||||
|
line = $0;
|
||||||
|
sub(/.*Domain:/, "", line);
|
||||||
|
gsub(/^ +/, "", line);
|
||||||
|
if (line != "" && line !~ /^WORKGROUP/)
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "Domain:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^\| [0-9]+\/tcp/ {
|
||||||
|
line = $0;
|
||||||
|
gsub(/^ *\| */, "", line);
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", "RPC:", line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/^[0-9]+\/tcp[[:space:]]+open/ {
|
||||||
|
port = $1; service = $3;
|
||||||
|
version = "";
|
||||||
|
for (i = 4; i <= NF; i++) version = version " " $i;
|
||||||
|
gsub(/^ +/, "", version);
|
||||||
|
if (version != "")
|
||||||
|
printf " \033[2m%-10s\033[0m %s — %s\n", port, service, version;
|
||||||
|
else
|
||||||
|
printf " \033[2m%-10s\033[0m %s\n", port, service;
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Done."
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: pos ssh load-keys
|
||||||
|
|
||||||
|
Load all SSH private keys into the ssh-agent.
|
||||||
|
|
||||||
|
Requires ssh-agent.service running (socket at /run/ssh-agent/socket).
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-/run/ssh-agent/socket}"
|
||||||
|
|
||||||
|
for key in ~/.ssh/id_*; do
|
||||||
|
[ -f "$key" ] || continue
|
||||||
|
case "$key" in
|
||||||
|
*.pub|known_hosts|authorized_keys|config) continue ;;
|
||||||
|
esac
|
||||||
|
ssh-keygen -y -f "$key" &>/dev/null || continue
|
||||||
|
ssh-add "$key" 2>/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
ssh-add -l
|
||||||
Executable
+284
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "ERROR: Please run as root (sudo)."
|
||||||
|
echo "Usage: sudo pos system firewall"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HISTORY=()
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--dry-run" ]]; then
|
||||||
|
DRY_RUN=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() { echo "[+] $*"; }
|
||||||
|
warn() { echo "[!] $*"; }
|
||||||
|
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
run_cmd() {
|
||||||
|
local -a cmd=("$@")
|
||||||
|
printf "\n>>> %s\n" "${cmd[*]}"
|
||||||
|
read -rp "Execute this command? [y/N]: " confirm
|
||||||
|
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||||
|
echo "(dry-run) skipping execution"
|
||||||
|
else
|
||||||
|
"${cmd[@]}"
|
||||||
|
fi
|
||||||
|
HISTORY+=("${cmd[*]}")
|
||||||
|
else
|
||||||
|
echo "Cancelled."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
build_ufw_cmd() {
|
||||||
|
local action="$1"
|
||||||
|
local direction="$2"
|
||||||
|
local proto="$3"
|
||||||
|
local from="$4"
|
||||||
|
local to="$5"
|
||||||
|
local port="$6"
|
||||||
|
local onif="$7"
|
||||||
|
local logmode="$8"
|
||||||
|
local comment="$9"
|
||||||
|
local insert_pos="${10:-}"
|
||||||
|
local suffix="${11:-}"
|
||||||
|
|
||||||
|
local -a cmd=(ufw)
|
||||||
|
|
||||||
|
if [[ -n "$insert_pos" ]]; then
|
||||||
|
if [[ "$insert_pos" == "prepend" ]]; then
|
||||||
|
cmd+=(prepend)
|
||||||
|
else
|
||||||
|
cmd+=(insert "$insert_pos")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmd+=("$action")
|
||||||
|
|
||||||
|
[[ -n "$direction" ]] && cmd+=("$direction")
|
||||||
|
[[ -n "$onif" ]] && cmd+=(on "$onif")
|
||||||
|
[[ -n "$proto" ]] && cmd+=(proto "$proto")
|
||||||
|
[[ -n "$from" ]] && cmd+=(from "$from")
|
||||||
|
cmd+=(to "$to")
|
||||||
|
[[ -n "$port" ]] && cmd+=(port "$port")
|
||||||
|
[[ -n "$logmode" ]] && cmd+=("$logmode")
|
||||||
|
if [[ -n "$comment" ]]; then
|
||||||
|
local safe="${comment// /_}"
|
||||||
|
cmd+=(comment "$safe")
|
||||||
|
fi
|
||||||
|
[[ "$suffix" == "v6" ]] && cmd+=(v6)
|
||||||
|
|
||||||
|
run_cmd "${cmd[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_ipver() {
|
||||||
|
local ver
|
||||||
|
read -rp "IP version (4 / 6 / both): " ver
|
||||||
|
echo "$ver"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_for_versions() {
|
||||||
|
local action="$1" direction="$2" proto="$3" from="$4" to="$5"
|
||||||
|
local port="$6" onif="$7" logmode="$8" comment="$9"
|
||||||
|
local insert_pos="${10:-}"
|
||||||
|
local ipver
|
||||||
|
ipver=$(prompt_ipver)
|
||||||
|
|
||||||
|
case "$ipver" in
|
||||||
|
4) build_ufw_cmd "$action" "$direction" "$proto" "$from" "$to" "$port" "$onif" "$logmode" "$comment" "$insert_pos" "" ;;
|
||||||
|
6) build_ufw_cmd "$action" "$direction" "$proto" "$from" "$to" "$port" "$onif" "$logmode" "$comment" "$insert_pos" "v6" ;;
|
||||||
|
both)
|
||||||
|
build_ufw_cmd "$action" "$direction" "$proto" "$from" "$to" "$port" "$onif" "$logmode" "$comment" "$insert_pos" ""
|
||||||
|
build_ufw_cmd "$action" "$direction" "$proto" "$from" "$to" "$port" "$onif" "$logmode" "$comment" "$insert_pos" "v6"
|
||||||
|
;;
|
||||||
|
*) echo "Invalid choice. Choose 4, 6 or both." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rule() {
|
||||||
|
echo
|
||||||
|
echo "Choose rule type:"
|
||||||
|
echo "1) Port/service (eg: port 8080 or 'ssh')"
|
||||||
|
echo "2) IP-based (from X to Y)"
|
||||||
|
echo "3) Directional port rule (in/out to any port ...)"
|
||||||
|
read -rp "Choice: " rtype
|
||||||
|
|
||||||
|
case "$rtype" in
|
||||||
|
1)
|
||||||
|
read -rp "Action (allow/deny/reject/limit) [allow]: " action
|
||||||
|
action=${action:-allow}
|
||||||
|
read -rp "Enter port number or service name (eg 'ssh' or '8080'): " port_or_svc
|
||||||
|
|
||||||
|
if [[ "$port_or_svc" =~ ^[0-9]+$ ]]; then
|
||||||
|
read -rp "Protocol (tcp/udp/any) [tcp]: " proto
|
||||||
|
proto=${proto:-tcp}
|
||||||
|
[[ "$proto" == "any" ]] && proto=""
|
||||||
|
read -rp "Interface (leave empty for any): " onif
|
||||||
|
read -rp "Log? (none/log/log-all) [none]: " logmode
|
||||||
|
[[ "$logmode" == "none" ]] && logmode=""
|
||||||
|
read -rp "Comment (optional): " comment
|
||||||
|
|
||||||
|
apply_for_versions "$action" "" "$proto" "" "any" "$port_or_svc" "$onif" "$logmode" "$comment"
|
||||||
|
else
|
||||||
|
read -rp "IP version (4 / 6 / both) [4]: " ipver
|
||||||
|
ipver=${ipver:-4}
|
||||||
|
case "$ipver" in
|
||||||
|
4) run_cmd ufw "$action" "$port_or_svc" ;;
|
||||||
|
6) run_cmd ufw "$action" "$port_or_svc" v6 ;;
|
||||||
|
both) run_cmd ufw "$action" "$port_or_svc"
|
||||||
|
run_cmd ufw "$action" "$port_or_svc" v6 ;;
|
||||||
|
*) echo "invalid ipver" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
2)
|
||||||
|
read -rp "Action (allow/deny/reject) [deny]: " action
|
||||||
|
action=${action:-deny}
|
||||||
|
read -rp "From address/CIDR (eg 192.168.1.5 or 10.0.0.0/24): " from
|
||||||
|
read -rp "To address (leave empty for 'any') [any]: " to
|
||||||
|
to=${to:-any}
|
||||||
|
read -rp "Direction (in/out) [in]: " direction
|
||||||
|
direction=${direction:-in}
|
||||||
|
read -rp "Port (leave empty if not applicable): " port
|
||||||
|
read -rp "Protocol (tcp/udp/any) [any]: " proto
|
||||||
|
[[ "$proto" == "any" ]] && proto=""
|
||||||
|
read -rp "Interface (leave empty for any): " onif
|
||||||
|
read -rp "Log? (none/log/log-all) [none]: " logmode
|
||||||
|
[[ "$logmode" == "none" ]] && logmode=""
|
||||||
|
read -rp "Comment (optional): " comment
|
||||||
|
|
||||||
|
apply_for_versions "$action" "$direction" "$proto" "$from" "$to" "$port" "$onif" "$logmode" "$comment"
|
||||||
|
;;
|
||||||
|
|
||||||
|
3)
|
||||||
|
read -rp "Action (allow/deny/reject/limit) [allow]: " action
|
||||||
|
action=${action:-allow}
|
||||||
|
read -rp "Direction (in/out) [in]: " direction
|
||||||
|
direction=${direction:-in}
|
||||||
|
read -rp "Port number: " port
|
||||||
|
read -rp "Protocol (tcp/udp/any) [tcp]: " proto
|
||||||
|
[[ "$proto" == "any" ]] && proto=""
|
||||||
|
read -rp "On interface (leave empty for any): " onif
|
||||||
|
read -rp "From address (optional): " from
|
||||||
|
from=${from:-}
|
||||||
|
read -rp "To address [any]: " to
|
||||||
|
to=${to:-any}
|
||||||
|
read -rp "Log? (none/log/log-all) [none]: " logmode
|
||||||
|
[[ "$logmode" == "none" ]] && logmode=""
|
||||||
|
read -rp "Comment (optional): " comment
|
||||||
|
read -rp "Insert position (number/prepend/empty): " insert_pos
|
||||||
|
|
||||||
|
apply_for_versions "$action" "$direction" "$proto" "$from" "$to" "$port" "$onif" "$logmode" "$comment" "$insert_pos"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*) echo "Unknown choice." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_rule() {
|
||||||
|
echo
|
||||||
|
echo "Delete rule by:"
|
||||||
|
echo "1) rule number (use 'ufw status numbered' to see numbers)"
|
||||||
|
echo "2) rule text (eg: 'allow 22/tcp')"
|
||||||
|
read -rp "Choice: " dch
|
||||||
|
|
||||||
|
case "$dch" in
|
||||||
|
1)
|
||||||
|
ufw status numbered
|
||||||
|
read -rp "Number to delete: " num
|
||||||
|
run_cmd ufw delete "$num"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
read -rp "Exact rule text to delete (eg: deny 80/tcp): " ruletext
|
||||||
|
run_cmd ufw delete $ruletext
|
||||||
|
;;
|
||||||
|
*) echo "Unknown choice." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
echo
|
||||||
|
echo "1) Simple status"
|
||||||
|
echo "2) Verbose status"
|
||||||
|
echo "3) Numbered status (useful for delete)"
|
||||||
|
read -rp "Choice: " sc
|
||||||
|
case "$sc" in
|
||||||
|
1) run_cmd ufw status ;;
|
||||||
|
2) run_cmd ufw status verbose ;;
|
||||||
|
3) run_cmd ufw status numbered ;;
|
||||||
|
*) echo "Unknown choice." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
cat <<'MENU'
|
||||||
|
|
||||||
|
==============================
|
||||||
|
UFW POWER — human friendly
|
||||||
|
==============================
|
||||||
|
1) Add rule (port/service/ip/directional)
|
||||||
|
2) Delete rule (by number or text)
|
||||||
|
3) Show status (simple / verbose / numbered)
|
||||||
|
4) Enable UFW
|
||||||
|
5) Disable UFW
|
||||||
|
6) Reset UFW (delete all rules)
|
||||||
|
7) Set default policy (incoming/outgoing)
|
||||||
|
8) Show executed commands history (so far)
|
||||||
|
0) Exit
|
||||||
|
------------------------------
|
||||||
|
MENU
|
||||||
|
read -rp "Choose: " opt
|
||||||
|
|
||||||
|
case "$opt" in
|
||||||
|
1) add_rule ;;
|
||||||
|
2) delete_rule ;;
|
||||||
|
3) show_status ;;
|
||||||
|
4) run_cmd ufw enable ;;
|
||||||
|
5) run_cmd ufw disable ;;
|
||||||
|
6)
|
||||||
|
echo "WARNING: ufw reset will disable and remove all rules."
|
||||||
|
read -rp "Type 'RESET' to confirm: " c
|
||||||
|
[[ "$c" == "RESET" ]] && run_cmd ufw reset || echo "Reset aborted."
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
read -rp "Default incoming policy (allow/deny/reject) [deny]: " defin
|
||||||
|
defin=${defin:-deny}
|
||||||
|
read -rp "Default outgoing policy (allow/deny/reject) [allow]: " defout
|
||||||
|
defout=${defout:-allow}
|
||||||
|
run_cmd ufw default "$defin" incoming
|
||||||
|
run_cmd ufw default "$defout" outgoing
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
echo
|
||||||
|
echo "Executed commands so far:"
|
||||||
|
echo
|
||||||
|
if [[ "${#HISTORY[@]}" -eq 0 ]]; then
|
||||||
|
echo "(none yet)"
|
||||||
|
else
|
||||||
|
for c in "${HISTORY[@]}"; do echo " - $c"; done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
echo
|
||||||
|
echo "Final executed commands summary:"
|
||||||
|
if [[ "${#HISTORY[@]}" -eq 0 ]]; then
|
||||||
|
echo "(no commands executed)"
|
||||||
|
else
|
||||||
|
for c in "${HISTORY[@]}"; do echo " - $c"; done
|
||||||
|
fi
|
||||||
|
echo "Goodbye — firewall remains watchful."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown option." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -rp "Press Enter to continue..."
|
||||||
|
clear
|
||||||
|
done
|
||||||
Executable
+156
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
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
|
||||||
|
|
||||||
|
Manage disposable Docker containers as lightweight VMs.
|
||||||
|
|
||||||
|
Each container gets a bind-mounted host directory so files persist
|
||||||
|
on the host even after the container is removed.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dir <path> Use custom directory instead of default ~/<name>
|
||||||
|
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
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cmd="${1:-}"
|
||||||
|
[ -z "$cmd" ] && usage
|
||||||
|
|
||||||
|
container_exists() {
|
||||||
|
docker container inspect "$1" &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
container_running() {
|
||||||
|
[[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null)" == "true" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
create)
|
||||||
|
name="${2:-}"
|
||||||
|
[ -z "$name" ] && usage
|
||||||
|
|
||||||
|
# Parse remaining args: [image] [--dir <path>]
|
||||||
|
image="ubuntu:22.04"
|
||||||
|
custom_dir=""
|
||||||
|
shift 2 || true
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--dir)
|
||||||
|
[ -z "${2:-}" ] && { echo "Missing value for --dir"; exit 1; }
|
||||||
|
custom_dir="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
image="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if container_exists "$name"; then
|
||||||
|
echo "[!] Container already exists: $name"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$custom_dir" ]; then
|
||||||
|
lab_dir="$(cd "$custom_dir" 2>/dev/null && pwd)" || { echo "[!] Directory not found: $custom_dir"; exit 1; }
|
||||||
|
else
|
||||||
|
lab_dir="$HOME/$name"
|
||||||
|
fi
|
||||||
|
mkdir -p "$lab_dir"
|
||||||
|
echo "[+] Lab directory: $lab_dir"
|
||||||
|
|
||||||
|
echo "[+] Pulling image: $image"
|
||||||
|
docker pull "$image"
|
||||||
|
|
||||||
|
echo "[+] Creating: $name"
|
||||||
|
docker create \
|
||||||
|
-it \
|
||||||
|
--name "$name" \
|
||||||
|
--label mylinux.vbox=true \
|
||||||
|
-v "$lab_dir:$lab_dir" \
|
||||||
|
-w "$lab_dir" \
|
||||||
|
"$image" \
|
||||||
|
bash >/dev/null
|
||||||
|
echo "[+] Done"
|
||||||
|
|
||||||
|
if confirm "Enter now?"; then
|
||||||
|
docker start "$name" >/dev/null
|
||||||
|
exec docker exec -it -w "$lab_dir" "$name" bash
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
enter)
|
||||||
|
name="${2:-}"
|
||||||
|
[ -z "$name" ] && usage
|
||||||
|
|
||||||
|
if ! container_exists "$name"; then
|
||||||
|
echo "[!] Container not found: $name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! container_running "$name"; then
|
||||||
|
docker start "$name" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect working dir from container mounts
|
||||||
|
lab_dir=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination .Destination}}{{.Source}}{{end}}{{end}}' "$name" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$lab_dir" ] && [ -d "$lab_dir" ]; then
|
||||||
|
exec docker exec -it -w "$lab_dir" "$name" bash
|
||||||
|
else
|
||||||
|
exec docker exec -it "$name" bash
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
start)
|
||||||
|
name="${2:-}"
|
||||||
|
[ -z "$name" ] && usage
|
||||||
|
docker start "$name"
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
name="${2:-}"
|
||||||
|
[ -z "$name" ] && usage
|
||||||
|
docker stop "$name"
|
||||||
|
;;
|
||||||
|
|
||||||
|
rm)
|
||||||
|
name="${2:-}"
|
||||||
|
[ -z "$name" ] && usage
|
||||||
|
docker rm -f "$name"
|
||||||
|
;;
|
||||||
|
|
||||||
|
ls)
|
||||||
|
docker ps -a --filter label=mylinux.vbox=true --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec pos ssh load-keys "$@"
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec pos network checkport "$@"
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec pos docker compose "$@"
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec pos docker ps "$@"
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec pos network scan "$@"
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exec pos system firewall "$@"
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/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/
|
||||||
|
|
||||||
|
_pos() {
|
||||||
|
local cur prev words cword
|
||||||
|
|
||||||
|
# Manual init if bash-completion package is not loaded
|
||||||
|
if declare -F _init_completion &>/dev/null; then
|
||||||
|
_init_completion || return
|
||||||
|
else
|
||||||
|
COMPREPLY=()
|
||||||
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
|
words=("${COMP_WORDS[@]}")
|
||||||
|
cword=$COMP_CWORD
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pos_bin="${COMP_WORDS[0]}"
|
||||||
|
local pos_dir
|
||||||
|
pos_dir="$(dirname "$(command -v "$pos_bin" 2>/dev/null || echo "$pos_bin")")"
|
||||||
|
|
||||||
|
# ── Collect all pos-* subcommands ──────────────────────────
|
||||||
|
local all_cmds=()
|
||||||
|
local f
|
||||||
|
for f in "$pos_dir"/pos-*; do
|
||||||
|
[ -x "$f" ] || continue
|
||||||
|
all_cmds+=("${f##*/pos-}")
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Build category→subcommand map ──────────────────────────
|
||||||
|
local -A cat_cmds
|
||||||
|
for cmd in "${all_cmds[@]}"; do
|
||||||
|
local cat="${cmd%%-*}"
|
||||||
|
local sub="${cmd#*-}"
|
||||||
|
if [ "$cat" != "$cmd" ]; then
|
||||||
|
cat_cmds["$cat"]+="${sub} "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Helpers ────────────────────────────────────────────────
|
||||||
|
_pos_complete_categories() {
|
||||||
|
COMPREPLY=($(compgen -W "${!cat_cmds[*]}" -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos_complete_subcats() {
|
||||||
|
local cat="${words[1]}"
|
||||||
|
COMPREPLY=($(compgen -W "${cat_cmds[$cat]:-}" -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos_complete_compose_services() {
|
||||||
|
local scale_dir="/usr/local/share/mylinux/scale-tail/services"
|
||||||
|
if [ -d "$scale_dir" ]; then
|
||||||
|
local svcs=()
|
||||||
|
for d in "$scale_dir"/*/; do
|
||||||
|
[ -d "$d" ] && svcs+=("$(basename "$d")")
|
||||||
|
done
|
||||||
|
COMPREPLY=($(compgen -W "${svcs[*]}" -- "$cur"))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos_complete_compose_cmds() {
|
||||||
|
COMPREPLY=($(compgen -W "ls installed up down restart logs update config" -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos_complete_vbox_cmds() {
|
||||||
|
COMPREPLY=($(compgen -W "create enter stop start rm ls" -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos_complete_docker_vbox_names() {
|
||||||
|
local names
|
||||||
|
names=$(docker ps -a --filter label=mylinux.vbox=true --format '{{.Names}}' 2>/dev/null)
|
||||||
|
COMPREPLY=($(compgen -W "$names" -- "$cur"))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dispatch ───────────────────────────────────────────────
|
||||||
|
case "${#words[@]}" in
|
||||||
|
2)
|
||||||
|
_pos_complete_categories
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
_pos_complete_subcats
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
case "${words[1]}-${words[2]}" in
|
||||||
|
docker-compose)
|
||||||
|
case "${words[3]}" in
|
||||||
|
up|down|restart|logs)
|
||||||
|
_pos_complete_compose_services
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
_pos_complete_compose_cmds
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
vbox-*)
|
||||||
|
_pos_complete_vbox_cmds
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
case "${words[1]}-${words[2]}" in
|
||||||
|
docker-compose)
|
||||||
|
case "${words[3]}" in
|
||||||
|
up|down|restart|logs)
|
||||||
|
_pos_complete_compose_services
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
vbox-create|vbox-enter|vbox-stop|vbox-start|vbox-rm)
|
||||||
|
_pos_complete_docker_vbox_names
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
complete -F _pos pos
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK8u4mvN1oWUGjWZCjUT0742u6AJEIHfi+PAQgZuNnPv
|
||||||
Executable
+144
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Parse --no-color BEFORE sourcing common.sh ──────────────────
|
||||||
|
NO_COLOR=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
[ "$arg" = "--no-color" ] && NO_COLOR=1
|
||||||
|
done
|
||||||
|
if [ "$NO_COLOR" -eq 1 ]; then
|
||||||
|
export TERM=dumb
|
||||||
|
unset CYAN GREEN YELLOW RED BLUE BOLD RESET
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "$(dirname "$0")/lib/common.sh"
|
||||||
|
|
||||||
|
DRY_RUN=0
|
||||||
|
RUN_APPS=0
|
||||||
|
SKIP_PHASES=""
|
||||||
|
STEPS_SPEC=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ./install.sh [OPTIONS]
|
||||||
|
|
||||||
|
Bootstrap a fresh Debian/Ubuntu install.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--apps Run interactive app picker after core install
|
||||||
|
--full Core install + all optional apps (non-interactive)
|
||||||
|
--dry-run Show what would be done 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
|
||||||
|
(1=preinstall, 2=scripts, 3=postinstall, 4=scalepoint)
|
||||||
|
--no-color Disable colored output
|
||||||
|
-h, --help Show this help message
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--apps) RUN_APPS=1; shift ;;
|
||||||
|
--full) RUN_APPS=2; shift ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
--skip)
|
||||||
|
[ -z "${2:-}" ] && err "Missing value for --skip"
|
||||||
|
SKIP_PHASES="${SKIP_PHASES:+$SKIP_PHASES,}$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--steps)
|
||||||
|
[ -z "${2:-}" ] && err "Missing value for --steps"
|
||||||
|
STEPS_SPEC="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-color) shift ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) err "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Phase runner ────────────────────────────────────────────────
|
||||||
|
# Phase names → numbers: preinstall=1 scripts=2 postinstall=3 scalepoint=4
|
||||||
|
should_run() {
|
||||||
|
local phase_num="$1"
|
||||||
|
local phase_name="$2"
|
||||||
|
|
||||||
|
# --skip takes precedence
|
||||||
|
if [[ ",$SKIP_PHASES," == *",$phase_name,"* ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --steps restricts to listed phases only
|
||||||
|
if [ -n "$STEPS_SPEC" ]; then
|
||||||
|
if [[ ",$STEPS_SPEC," != *",$phase_num,"* ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
section "myLinux Bootstrap"
|
||||||
|
timer_start
|
||||||
|
|
||||||
|
# ── Phase 1: preinstall ────────────────────────────────────────
|
||||||
|
if should_run 1 preinstall; then
|
||||||
|
step 1 4 "Installing system packages"
|
||||||
|
if [ -f preinstall.sh ]; then
|
||||||
|
spawn "apt update" sudo apt update
|
||||||
|
bash preinstall.sh
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 2: install wrappers ──────────────────────────────────
|
||||||
|
if should_run 2 scripts; then
|
||||||
|
step 2 4 "Installing wrapper scripts"
|
||||||
|
run sudo mkdir -p /usr/local/bin
|
||||||
|
count=0
|
||||||
|
for f in bin/*; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
run sudo install -m 755 "$f" /usr/local/bin/
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
run sudo install -m 644 lib/common.sh /usr/local/bin/common.sh
|
||||||
|
ok "$count scripts + lib -> /usr/local/bin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 3: postinstall ───────────────────────────────────────
|
||||||
|
if should_run 3 postinstall; then
|
||||||
|
step 3 4 "Post-install configuration"
|
||||||
|
if [ -f postinstall.sh ]; then
|
||||||
|
bash postinstall.sh
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Phase 4: ScaleTail templates ────────────────────────────────
|
||||||
|
if should_run 4 scalepoint; then
|
||||||
|
step 4 4 "Cloning ScaleTail templates"
|
||||||
|
scale_dest="/usr/local/share/mylinux/scale-tail"
|
||||||
|
if [ ! -d "$scale_dest" ]; then
|
||||||
|
spawn "Cloning ScaleTail" sudo git clone --depth 1 \
|
||||||
|
https://github.com/tailscale-dev/ScaleTail.git "$scale_dest"
|
||||||
|
else
|
||||||
|
log "ScaleTail already cloned"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
|
echo "${GREEN} Bootstrap complete ($(timer_stop))${RESET}"
|
||||||
|
echo "${GREEN}════════════════════════════════════════════${RESET}"
|
||||||
|
|
||||||
|
# ── Optional apps ──────────────────────────────────────────────
|
||||||
|
if [ "$RUN_APPS" -eq 1 ]; then
|
||||||
|
echo
|
||||||
|
bash apps/install.sh
|
||||||
|
elif [ "$RUN_APPS" -eq 2 ]; then
|
||||||
|
echo
|
||||||
|
bash apps/install.sh --all
|
||||||
|
elif should_run 5 apps && [ -f apps/install.sh ]; then
|
||||||
|
# --skip apps disables app phase even if --apps/--full is not used
|
||||||
|
true
|
||||||
|
fi
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
# ── Colors (auto-off when not a TTY) ───────────────────────────
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
CYAN=$(tput setaf 6)
|
||||||
|
GREEN=$(tput setaf 2)
|
||||||
|
YELLOW=$(tput setaf 3)
|
||||||
|
RED=$(tput setaf 1)
|
||||||
|
BLUE=$(tput setaf 4)
|
||||||
|
BOLD=$(tput bold)
|
||||||
|
RESET=$(tput sgr0)
|
||||||
|
else
|
||||||
|
CYAN=""; GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; RESET=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Core helpers ───────────────────────────────────────────────
|
||||||
|
log() { echo "${GREEN}[+]${RESET} $*"; }
|
||||||
|
warn() { echo "${YELLOW}[!]${RESET} $*"; }
|
||||||
|
err() { echo "${RED}ERROR:${RESET} $*" >&2; exit 1; }
|
||||||
|
ok() { echo "${GREEN} OK${RESET} $*"; }
|
||||||
|
|
||||||
|
# ── Section header ─────────────────────────────────────────────
|
||||||
|
section() {
|
||||||
|
local title="$*"
|
||||||
|
echo
|
||||||
|
echo "${CYAN}════════════════════════════════════════════${RESET}"
|
||||||
|
echo "${CYAN} ${title}${RESET}"
|
||||||
|
echo "${CYAN}════════════════════════════════════════════${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Step header (numbered) ─────────────────────────────────────
|
||||||
|
step() {
|
||||||
|
local current="$1" total="$2" msg="$3"
|
||||||
|
echo
|
||||||
|
echo "${BOLD} [${current}/${total}] ${msg}${RESET}"
|
||||||
|
echo "${BLUE} ─────────────────────────────────────────${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dry-run aware executor ─────────────────────────────────────
|
||||||
|
run() {
|
||||||
|
if [ "${DRY_RUN:-0}" -eq 1 ]; then
|
||||||
|
log "(dry-run) $*"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Internal: nanoseconds → formatted time string ─────────────
|
||||||
|
_nano_now() { date +%s%N; }
|
||||||
|
_elapsed() {
|
||||||
|
local start="$1" end
|
||||||
|
end=$(_nano_now)
|
||||||
|
local ms=$(( (end - start) / 1000000 ))
|
||||||
|
if [ "$ms" -ge 1000 ]; then
|
||||||
|
awk "BEGIN { printf \"%.1fs\", $ms / 1000 }"
|
||||||
|
elif [ "$ms" -ge 1 ]; then
|
||||||
|
echo "${ms}ms"
|
||||||
|
else
|
||||||
|
echo "0ms"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Timer ──────────────────────────────────────────────────────
|
||||||
|
TIMER_START=0
|
||||||
|
timer_start() { TIMER_START=$(_nano_now); }
|
||||||
|
timer_stop() { _elapsed "$TIMER_START"; }
|
||||||
|
|
||||||
|
# ── Timed command runner ───────────────────────────────────────
|
||||||
|
# Shows a spinner while the command runs in background,
|
||||||
|
# then prints result + elapsed time.
|
||||||
|
spawn() {
|
||||||
|
local msg="$1"
|
||||||
|
shift
|
||||||
|
local start
|
||||||
|
start=$(_nano_now)
|
||||||
|
|
||||||
|
# Run in background, capture output
|
||||||
|
local out err rc
|
||||||
|
out=$(mktemp)
|
||||||
|
err=$(mktemp)
|
||||||
|
"$@" >"$out" 2>"$err" &
|
||||||
|
local pid=$!
|
||||||
|
|
||||||
|
# Spinner
|
||||||
|
local spin=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||||
|
local i=0
|
||||||
|
while kill -0 "$pid" 2>/dev/null; do
|
||||||
|
printf "\r${CYAN} %s${RESET} %s" "${spin[$i]}" "$msg"
|
||||||
|
i=$(( (i + 1) % ${#spin[@]} ))
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
rc=0; wait "$pid" || rc=$?
|
||||||
|
local elapsed
|
||||||
|
elapsed=$(_elapsed "$start")
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
printf "\r${GREEN} OK${RESET} %s (${elapsed})\n" "$msg"
|
||||||
|
else
|
||||||
|
printf "\r${RED} FAIL${RESET} %s (${elapsed})\n" "$msg"
|
||||||
|
# Show captured stderr on failure
|
||||||
|
if [ -s "$err" ]; then
|
||||||
|
sed 's/^/ /' "$err"
|
||||||
|
fi
|
||||||
|
rm -f "$out" "$err"
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
rm -f "$out" "$err"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Confirmation prompt ────────────────────────────────────────
|
||||||
|
confirm() {
|
||||||
|
local prompt="$1" default="${2:-y}" yn
|
||||||
|
if [ "$default" = "y" ]; then
|
||||||
|
read -rp "${prompt} [Y/n]: " yn
|
||||||
|
[[ -z "$yn" || "$yn" =~ ^[Yy] ]]
|
||||||
|
else
|
||||||
|
read -rp "${prompt} [y/N]: " yn
|
||||||
|
[[ "$yn" =~ ^[Yy] ]]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Source guard ───────────────────────────────────────────────
|
||||||
|
return 0 2>/dev/null || true
|
||||||
Executable
+94
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/lib/common.sh"
|
||||||
|
|
||||||
|
log "Running post-install..."
|
||||||
|
|
||||||
|
# ── rclone config ──────────────────────────────────────────────
|
||||||
|
# Place your rclone.conf in config/ (gitignored) and this will install it.
|
||||||
|
if [ -f config/rclone.conf ]; then
|
||||||
|
mkdir -p "$HOME/.config/rclone"
|
||||||
|
cp config/rclone.conf "$HOME/.config/rclone/rclone.conf"
|
||||||
|
chmod 600 "$HOME/.config/rclone/rclone.conf"
|
||||||
|
log "Installed rclone.conf"
|
||||||
|
else
|
||||||
|
warn "fail2ban not found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure all bin dirs are in PATH ────────────────────────────
|
||||||
|
PATH_LINE='export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$HOME/.local/bin:$PATH"'
|
||||||
|
BASHRC="$HOME/.bashrc"
|
||||||
|
|
||||||
|
if grep -qsF "$PATH_LINE" "$BASHRC" 2>/dev/null; then
|
||||||
|
log "PATH already configured"
|
||||||
|
else
|
||||||
|
echo "$PATH_LINE" >> "$BASHRC"
|
||||||
|
log "Added PATH to ~/.bashrc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Bash completion for pos ─────────────────────────────────────
|
||||||
|
COMPLETION_LINE='source /usr/local/share/bash-completion/completions/pos.bash 2>/dev/null || true'
|
||||||
|
|
||||||
|
if grep -qsF "pos.bash" "$BASHRC" 2>/dev/null; then
|
||||||
|
log "pos completion already configured"
|
||||||
|
else
|
||||||
|
echo "$COMPLETION_LINE" >> "$BASHRC"
|
||||||
|
log "Added pos completion to ~/.bashrc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f completions/pos.bash ]; then
|
||||||
|
run sudo mkdir -p /usr/local/share/bash-completion/completions
|
||||||
|
run sudo install -m 644 completions/pos.bash \
|
||||||
|
/usr/local/share/bash-completion/completions/pos.bash
|
||||||
|
log "Installed pos completion"
|
||||||
|
else
|
||||||
|
warn "completions/pos.bash not found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
# ── SSH authorized keys ────────────────────────────────────────
|
||||||
|
SSH_DIR="$HOME/.ssh"
|
||||||
|
AUTH_FILE="$SSH_DIR/authorized_keys"
|
||||||
|
KEY_FILE="config/authorized_keys"
|
||||||
|
|
||||||
|
if [ -f "$KEY_FILE" ]; then
|
||||||
|
mkdir -p "$SSH_DIR"
|
||||||
|
chmod 700 "$SSH_DIR"
|
||||||
|
touch "$AUTH_FILE"
|
||||||
|
chmod 600 "$AUTH_FILE"
|
||||||
|
|
||||||
|
added=0
|
||||||
|
while IFS= read -r key; do
|
||||||
|
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||||
|
if grep -qsF "$key" "$AUTH_FILE" 2>/dev/null; then
|
||||||
|
log "SSH key already present"
|
||||||
|
else
|
||||||
|
echo "$key" >> "$AUTH_FILE"
|
||||||
|
added=$((added + 1))
|
||||||
|
fi
|
||||||
|
done < "$KEY_FILE"
|
||||||
|
if [ "$added" -gt 0 ]; then
|
||||||
|
log "Installed $added SSH key(s)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "config/authorized_keys not found, skipping SSH setup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
=======
|
||||||
|
>>>>>>> bba577c (Initial commit)
|
||||||
|
# ── systemd services ───────────────────────────────────────────
|
||||||
|
if [ -d systemd ] && [ -n "$(ls -A systemd/*.service 2>/dev/null)" ]; then
|
||||||
|
run sudo cp systemd/*.service /etc/systemd/system/
|
||||||
|
run sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
for svc in systemd/*.service; do
|
||||||
|
svc_name=$(basename "$svc")
|
||||||
|
run sudo systemctl enable --now "$svc_name" 2>/dev/null || \
|
||||||
|
run sudo systemctl enable "$svc_name"
|
||||||
|
done
|
||||||
|
log "Systemd services installed and enabled"
|
||||||
|
else
|
||||||
|
warn "No systemd services found, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Post-install completed."
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
source "$(dirname "$0")/lib/common.sh"
|
||||||
|
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: preinstall.sh [OPTIONS]
|
||||||
|
|
||||||
|
Install system packages and tools.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run Show what would be done without executing
|
||||||
|
-h, --help Show this help message
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) err "Unknown option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
ca-certificates gnupg lsb-release
|
||||||
|
python3 python3-pip rclone
|
||||||
|
)
|
||||||
|
|
||||||
|
spawn "apt update" sudo apt update
|
||||||
|
spawn "Installing packages" sudo apt install -y "${PACKAGES[@]}"
|
||||||
|
#spawn "add user to sudo list" usermod -aG sudo $USER
|
||||||
|
spawn "Installing yt-dlp" sudo curl -L \
|
||||||
|
https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp \
|
||||||
|
-o /usr/local/bin/yt-dlp
|
||||||
|
run sudo chmod a+rx /usr/local/bin/yt-dlp
|
||||||
|
|
||||||
|
log "Verifying installations..."
|
||||||
|
for cmd in git yt-dlp; do
|
||||||
|
if command -v "$cmd" &>/dev/null; then
|
||||||
|
run "$cmd" --version
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Pre-install completed."
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[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
|
||||||
Reference in New Issue
Block a user