13 KiB
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-tailon 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) |
=======
| .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 bywr-compose config|
How to Add a New Tool
1. Create the script in bin/
#!/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 pipefailat the top --helpflag: all tools must accept-h/--help— use thecase ... esacpattern above- Shared library: source
lib/common.shfrom any script inbin/orapps/for consistent colors, logging (log,warn,err,ok), spinners (spawn), and dry-run support (run). Usespawn "message" commandfor long-running installs. - Fallback (no lib): if sourcing
common.shis not desired, inline:log() { echo "[+] $*"; } warn() { echo "[!] $*"; } err() { echo "ERROR: $*" >&2; exit 1; } - Exit codes:
0for success,1for error
2. Add system dependencies (if any)
Open preinstall.sh and add the package name to the PACKAGES array:
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
# 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
- Find the script — all tools live in
bin/ - Understand the contract — what args does it expect? What does it print? What exit codes?
- Make the change — keep it idempotent if possible (running twice = same result)
- Update README if usage, output, or behaviour changed
- Run
shellcheckon the modified file: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
#!/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.shfrom the app directory - Idempotent: check
command -vbefore installing; skip if present - Method: standardize on official repos/scripts over PPAs or third-party
- APT packages →
sudo apt install -y <pkg>wrapped inspawn - Official scripts →
curl ... | shinsidespawn - Flatpak →
flatpak install -y flathub <app-id>insidespawn .debfiles → download to temp andsudo apt install -y ./file.debinsidespawn- Groups:
usermodcommands 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
# 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
aptnotapt-getunless you need non-interactive guarantees - Assume
bashis 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:
./install.sh --dry-run # preview without executing
Use the run() helper pattern:
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.gitignored - Use
chmod 600for sensitive files (SSH keys, rclone config) - Validate user input before using it in shell commands
- Use
sudoonly 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
- Create
systemd/<name>.service - postinstall.sh automatically copies all
*.servicefiles to/etc/systemd/system/and enables them
Service file template:
[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 a section 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"
echo "Installed your-config.conf"
fi
Working with Docker Compose
The installer clones 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 forTS_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 updaterefreshes onlycompose.yamlfrom the templates;.envfiles are preserved. - Services path: set
SERVICES_BASEto any directory (e.g./srv) viawr-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. To add a service:
- Fork ScaleTail and add your service under
services/<name>/ - Submit a PR upstream
- 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
# 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