- README: reorganized for users (what → quickstart → tools → apps) removed redundant Goal section, Shared Library section clearer flag table, better scan-friendly tool reference - DEV.md: reorganized for contributors (architecture → how-to → best practices) condensed Docker Compose section, clearer headings removed duplicate content, improved flow
9.0 KiB
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
#!/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 --helpflag: accept-h/--helpviacasepattern- Shared library: always source
common.shfor colors, logging, spinners - Exit codes:
0success,1error - No shared lib? Inline fallbacks:
log() { echo "[+] $*"; } warn() { echo "[!] $*"; } err() { echo "ERROR: $*" >&2; exit 1; }
2. Add system dependencies
Add package names to the PACKAGES array in preinstall.sh:
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 README.
4. Add SSH keys (if needed)
Place public keys in config/authorized_keys (one per line). postinstall.sh reads this file automatically.
5. Update README
Add a section under the relevant category in README.md.
6. Test
bash -n bin/your-tool
shellcheck bin/your-tool
./bin/your-tool --help
Adding an Optional App
1. Create the installer
#!/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
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 -vbefore installing - APT packages →
sudo apt install -yinsidespawn - Official scripts →
curl ... | shinsidespawn - Flatpak →
flatpak install -y flathub <app-id>insidespawn .debfiles → download to temp,sudo apt install -y ./file.debinsidespawnusermodfor groups → print re-login reminder
Editing an Existing Tool
- Find the script in
bin/ - Understand its contract (args, output, exit codes)
- Make the change — keep it idempotent
- Update README if behaviour changed
- Run
shellcheckon the modified file
Best Practices
Idempotency
Check before creating, use >> with grep guards, don't overwrite user configs.
Error Handling
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:
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 600for sensitive files- Validate input before shell commands
- Use
sudoonly where needed
Naming
- CLI tools:
bin/pos-<category>-<command> - Legacy wrappers:
bin/wr-* - App installers:
apps/<category>/<name>.sh - Lowercase with hyphens
Working with Systemd
Create systemd/<name>.service — postinstall.sh copies it to /etc/systemd/system/ and enables it automatically.
[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
- Place the file in
config/ - Add copy logic to
postinstall.sh:
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 templates to /usr/local/share/linux_post_install/scale-tail/ — 119+ self-hosted services with a Tailscale sidecar pattern.
Deployed Layout
/usr/local/share/linux_post_install/scale-tail/ # Templates (git repo)
└── services/<name>/
├── compose.yaml
└── .env
~/.config/linux_post_install/compose.env # Global defaults
/srv/<service>/ # Active deployment
├── compose.yaml # Refreshed on update (preserves .env)
├── .env # Your config — preserved across updates
├── config/
└── data/
Key Commands
| Command | Behaviour |
|---|---|
pos docker compose up <service> |
Deploys to $SERVICES_BASE/<service>/, creates config/ + data/, generates .env |
pos docker compose down <service> |
Stops the stack |
pos docker compose update |
git pull templates + refreshes compose.yaml for all deployed services |
pos docker compose config set K=V |
Sets global default in ~/.config/linux_post_install/compose.env |
.env Design
- Global config:
~/.config/linux_post_install/compose.env— one place forTS_AUTHKEY,TZ,DNS_SERVER,SERVICES_BASE - Per-service:
<SERVICES_BASE>/<service>/.env— generated from template, filled from global config - Updates:
compose.yamlrefreshes from template but.envis never overwritten
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
# 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