.
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