docs: reorganize into DOC/ and simplify README
- Move all docs (DEV, AGENT_Context, algorithm) into DOC/ via git mv - Rewrite root README as short intro + quick start + links into DOC/ - Add per-area reference docs: SCRIPTS (core scripts + libs + features), POS (pos CLI + compose config), APPS (picker + catalog), SYSTEMD (units) - Add DOC/README.md index; fix all cross-references
This commit is contained in:
+415
@@ -0,0 +1,415 @@
|
||||
# Development Guide
|
||||
|
||||
How this repo works, how to add features, and what to keep in mind when editing.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Installation Phases
|
||||
|
||||
```
|
||||
install.sh
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
▼ ▼ ▼
|
||||
preinstall.sh bin/* postinstall.sh
|
||||
(packages) → /usr/local/bin (config + services)
|
||||
```
|
||||
|
||||
| Phase | Script | Responsibility |
|
||||
|-------|--------|----------------|
|
||||
| Pre | `preinstall.sh` | System packages, apt repos, global binaries (yt-dlp) |
|
||||
| Install | `install.sh` | Copies `bin/*` → `/usr/local/bin/` (chmod 755), `lib/common.sh` → `/usr/local/bin/common.sh` |
|
||||
| Post | `postinstall.sh` | User config (SSH keys, PATH, bash completion), systemd services |
|
||||
|
||||
Each phase is independent and runs only if the corresponding script exists.
|
||||
|
||||
### Directory Layout
|
||||
|
||||
| Directory | Purpose | Installed To |
|
||||
|-----------|---------|-------------|
|
||||
| `bin/` | Daily-use CLI tools and wrappers | `/usr/local/bin/` |
|
||||
| `apps/<category>/` | Optional desktop app installers | run on demand |
|
||||
| `lib/` | Shared library (`common.sh`) | sourced at build time |
|
||||
| `config/` | Gitignored user config files | `~/.config/<app>/` (via postinstall) |
|
||||
| `compose/` | ScaleTail templates (git submodule) | `/usr/local/share/linux_post_install/scale-tail` |
|
||||
| `systemd/` | Systemd unit files | `/etc/systemd/system/` (via postinstall) |
|
||||
|
||||
### The `pos` CLI
|
||||
|
||||
`bin/pos` is a smart dispatcher. It scans its own directory for executable `pos-*` files and uses variable-length argument matching:
|
||||
|
||||
```
|
||||
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"
|
||||
```
|
||||
|
||||
All non-interactive commands log to `~/.local/share/linux_post_install/logs/`.
|
||||
|
||||
### Shared Library (`lib/common.sh`)
|
||||
|
||||
Sourced by most scripts. Key functions:
|
||||
|
||||
| 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 |
|
||||
| `run cmd` | Executes command, respects `$DRY_RUN` |
|
||||
| `spawn "msg" cmd` | Animated braille spinner + elapsed time |
|
||||
| `timer_start` / `timer_stop` | Elapsed time tracking |
|
||||
| `confirm "prompt"` | y/N prompt with optional default |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New CLI Tool
|
||||
|
||||
### 1. Create the script
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
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:**
|
||||
- Shebang: `#!/usr/bin/env bash`
|
||||
- Strict mode: `set -euo pipefail`
|
||||
- `--help` flag: accept `-h` / `--help` via `case` pattern
|
||||
- Shared library: always source `common.sh` for colors, logging, spinners
|
||||
- Exit codes: `0` success, `1` error
|
||||
- No shared lib? Inline fallbacks:
|
||||
```bash
|
||||
log() { echo "[+] $*"; }
|
||||
warn() { echo "[!] $*"; }
|
||||
err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
```
|
||||
|
||||
### 2. Add system dependencies
|
||||
|
||||
Add package names to the `PACKAGES` array in `preinstall.sh`:
|
||||
|
||||
```bash
|
||||
PACKAGES=(
|
||||
...
|
||||
your-package
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Add config files (if needed)
|
||||
|
||||
Place defaults in `config/` and add copy logic to `postinstall.sh`. If they contain secrets, add to `.gitignore` and document in `DOC/`.
|
||||
|
||||
### 4. Add SSH keys (if needed)
|
||||
|
||||
Place public keys in `config/authorized_keys` (one per line). `postinstall.sh` reads this file automatically.
|
||||
|
||||
### 5. Update the docs
|
||||
|
||||
Add a section for the new command in `DOC/POS.md`.
|
||||
|
||||
### 6. Test
|
||||
|
||||
```bash
|
||||
bash -n bin/your-tool
|
||||
shellcheck bin/your-tool
|
||||
./bin/your-tool --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding an Optional App
|
||||
|
||||
### 1. Create the installer
|
||||
|
||||
```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
|
||||
}
|
||||
|
||||
uninstall_myapp() {
|
||||
command -v myapp &>/dev/null || { log "myapp not installed"; return 0; }
|
||||
spawn "Removing myapp" sudo apt purge -y myapp
|
||||
spawn "Cleaning up dependencies" sudo apt autoremove -y
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
uninstall) uninstall_myapp ;;
|
||||
*) install_myapp ;;
|
||||
esac
|
||||
```
|
||||
|
||||
Place it in `apps/<category>/<name>.sh`. It auto-appears in the picker — no registration needed.
|
||||
|
||||
**Categories:** `browsers`, `development`, `media`, `networking`, `remote-access`, `system`, `utilities`
|
||||
|
||||
### 2. Conventions
|
||||
|
||||
- Idempotent: check `command -v` (or `flatpak list` / file existence) before installing **and** uninstalling
|
||||
- Every app **must** provide an `uninstall_<name>()` function and dispatch on `uninstall` via the `case` above — `apps/install.sh --uninstall` depends on it
|
||||
- APT packages → `sudo apt install -y` inside `spawn`, remove with `sudo apt purge -y` + `sudo apt autoremove -y`
|
||||
- Repo-based apps (apt repo added at install) → also remove the `.list` file and keyring in uninstall
|
||||
- Official scripts → `curl ... | sh` inside `spawn`
|
||||
- Flatpak → `flatpak install -y flathub <app-id>` inside `spawn`, remove with `flatpak uninstall -y <app-id>`
|
||||
- `.deb` files → download to temp, `sudo apt install -y ./file.deb` inside `spawn`
|
||||
- File/AppImage installs → remove the installed files, symlinks, and desktop entries in uninstall
|
||||
- `usermod` for groups → print re-login reminder
|
||||
|
||||
---
|
||||
|
||||
## Editing an Existing Tool
|
||||
|
||||
1. Find the script in `bin/`
|
||||
2. Understand its contract (args, output, exit codes)
|
||||
3. Make the change — keep it idempotent
|
||||
4. Update `DOC/POS.md` (or the relevant doc) if behaviour changed
|
||||
5. Run `shellcheck` on the modified file
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Idempotency
|
||||
|
||||
Check before creating, use `>>` with grep guards, don't overwrite user configs.
|
||||
|
||||
### Error Handling
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
command -v docker &>/dev/null || { echo "docker not found"; exit 1; }
|
||||
[[ -n "${1:-}" ]] || { echo "Usage: my-tool <arg>"; exit 1; }
|
||||
```
|
||||
|
||||
### Portability
|
||||
|
||||
Targets **Debian** and **Ubuntu**. Use `apt`, assume bash at `/usr/bin/env bash`, check tools with `command -v`.
|
||||
|
||||
### Dry-run Support
|
||||
|
||||
Scripts support `--dry-run`. Use the `run()` helper:
|
||||
|
||||
```bash
|
||||
run() {
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
log "(dry-run) $*"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
run sudo apt install -y git
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
- Never hardcode secrets — put them in `config/` (gitignored)
|
||||
- `chmod 600` for sensitive files
|
||||
- Validate input before shell commands
|
||||
- Use `sudo` only where needed
|
||||
|
||||
### Naming
|
||||
|
||||
- CLI tools: `bin/pos-<category>-<command>`
|
||||
- Legacy wrappers: `bin/wr-*`
|
||||
- App installers: `apps/<category>/<name>.sh`
|
||||
- Features: `features/<name>.sh`
|
||||
- Lowercase with hyphens
|
||||
|
||||
---
|
||||
|
||||
## Features & Flags
|
||||
|
||||
`features/` holds scripts the user is likely to customize (e.g. `autostart.sh`). Unlike `bin/` (synced on every install), features are installed on demand and **never overwritten without asking**.
|
||||
|
||||
### Adding a Feature
|
||||
|
||||
1. Create `features/<name>.sh` following the CLI tool template (shebang, `set -euo pipefail`, `--help`).
|
||||
2. Nothing else is registered — `./install.sh --feature` auto-discovers it, copies it to `/usr/local/bin/`, asks before overwriting an existing file, and sets its flag.
|
||||
3. If the feature backs a systemd service, gate the service on the flag in `postinstall.sh` (see below).
|
||||
|
||||
### Flag System
|
||||
|
||||
System-wide flag store at `/usr/local/share/linux_post_install/flags/` (presence = set, content = optional value). Sourced via `lib/flags.sh` (or the installed `/usr/local/bin/flags.sh`):
|
||||
|
||||
```bash
|
||||
source "$(dirname "$0")/lib/flags.sh" 2>/dev/null || source "$(dirname "$0")/flags.sh"
|
||||
|
||||
flag_set autostart # green flag
|
||||
flag_set app "2.1" # green flag with a value
|
||||
flag_is_set autostart # test (0/1) — the primitive consumers use
|
||||
flag_value app # → "2.1"
|
||||
flag_list # names of all set flags
|
||||
flag_clear autostart
|
||||
```
|
||||
|
||||
Writes use `run` + `sudo`, so they respect `--dry-run`. CLI equivalents: `flag-reader`, `flag-set`, `flag-clear`.
|
||||
|
||||
**Example — service gated on a flag** (in `postinstall.sh`'s systemd loop):
|
||||
|
||||
```bash
|
||||
if [ "$svc_name" = "myapp.service" ] && ! flag_is_set myapp; then
|
||||
warn "myapp feature not installed — skipping myapp.service"
|
||||
continue
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Working with Systemd
|
||||
|
||||
Create `systemd/<name>.service` — `postinstall.sh` copies it to `/etc/systemd/system/` and enables it automatically.
|
||||
|
||||
```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 copy logic 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"
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Compose / ScaleTail
|
||||
|
||||
The installer clones [ScaleTail](https://github.com/tailscale-dev/ScaleTail) templates to `/usr/local/share/linux_post_install/scale-tail/` — 119+ self-hosted services with a Tailscale sidecar pattern. Each service gets a `tail-xxxxx.ts.net` URL via `network_mode: service:tailscale`.
|
||||
|
||||
### Config Strategy — Three Layers
|
||||
|
||||
Values cascade from least to most specific:
|
||||
|
||||
```
|
||||
Template .env (per-service defaults from ScaleTail)
|
||||
↓
|
||||
Global config (~/.config/linux_post_install/compose.env)
|
||||
↓
|
||||
Per-service .env (/srv/<service>/.env) — created on first deploy, NEVER overwritten
|
||||
```
|
||||
|
||||
On first `pos docker compose up <service>`:
|
||||
1. Template `.env` is copied to `/srv/<service>/.env`
|
||||
2. Matching keys from global config are filled in
|
||||
3. If `TS_AUTHKEY` is still empty, you're prompted to enter it
|
||||
4. After that, the per-service `.env` is **never touched** — not even by `update`
|
||||
|
||||
### Layout
|
||||
|
||||
| Path | Purpose | Mutability |
|
||||
|------|---------|------------|
|
||||
| `/usr/local/share/linux_post_install/scale-tail/services/<name>/` | ScaleTail templates (git repo) | Read-only |
|
||||
| `~/.config/linux_post_install/compose.env` | Your global defaults | Edit via `config set` or `config edit` |
|
||||
| `/srv/<service>/` | Active deployment | Per-service `.env` preserved forever |
|
||||
|
||||
### Key Commands
|
||||
|
||||
| Command | Behaviour |
|
||||
|---------|-----------|
|
||||
| `pos docker compose up <service>` | Deploys to `$SERVICES_BASE/<service>/`, creates `config/` + `data/`, generates `.env` from global defaults |
|
||||
| `pos docker compose down <service>` | Stops the stack |
|
||||
| `pos docker compose update` | `git pull` templates + refreshes `compose.yaml` for all deployed services (`.env` untouched) |
|
||||
| `pos docker compose config set K=V` | Sets a global default in `~/.config/linux_post_install/compose.env` |
|
||||
| `pos docker compose config show` | Displays current global config and `SERVICES_BASE` |
|
||||
| `pos docker compose config edit` | Opens global config in `$EDITOR` |
|
||||
|
||||
### Global Config Keys
|
||||
|
||||
| Key | Required | Default | Purpose |
|
||||
|-----|----------|---------|---------|
|
||||
| `TS_AUTHKEY` | Yes | — | Tailscale auth key for sidecar networking |
|
||||
| `TZ` | No | `Europe/Amsterdam` | Timezone for services |
|
||||
| `DNS_SERVER` | No | `9.9.9.9` | Custom DNS server |
|
||||
| `SERVICES_BASE` | No | `/srv` | Root directory for all deployments |
|
||||
|
||||
---
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
- Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
|
||||
- Explain *why*, not just *what*
|
||||
- One logical change per commit
|
||||
|
||||
```
|
||||
feat: add pos-disk-usage for monitoring disk space
|
||||
fix: pos-network-ip fails when no default route exists
|
||||
docs: add example output for pos-network-scan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# Syntax check a single script
|
||||
bash -n bin/my-script
|
||||
|
||||
# ShellCheck linting
|
||||
shellcheck bin/my-script
|
||||
|
||||
# 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
|
||||
|
||||
# Init submodule
|
||||
git submodule update --init
|
||||
|
||||
# Pull latest ScaleTail templates
|
||||
git submodule update --remote compose/scale-tail
|
||||
|
||||
# Test install in Docker
|
||||
docker run --rm -it -v $PWD:/repo ubuntu:22.04 bash
|
||||
# inside: cd /repo && ./install.sh
|
||||
|
||||
# Test app installers
|
||||
./apps/install.sh
|
||||
./apps/install.sh --all
|
||||
./apps/install.sh docker vscode
|
||||
```
|
||||
Reference in New Issue
Block a user