diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d3a9178 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,236 @@ +# CLAUDE.md — ubuntu-post-install contributor guide + +Context for adding or modifying services. Read this before touching any +service file so the result matches what's already here. + +## How the system works + +`setup.sh` sources `lib/common.sh` then globs every `services/*.sh` file. +Each service file self-registers and defines its install function. Nothing +in `setup.sh` needs to change when you add a service — just add the file. + +The wizard groups services by category (from `register_service`), shows a +checklist per group, and calls `install_()` for each selected item. +`--list`, `--dry-run`, and `--unattended` all work automatically. + +## Adding a service — the three-step rule + +1. Create `services/.sh` (kebab-case filename) +2. Call `register_service` at the top of the file +3. Define `install_()` (hyphens → underscores in function name) + +That's it. The menu picks it up on the next run. + +## Minimal Docker service template + +```bash +#!/bin/bash +# services/my-tool.sh — One-line description. +# Part of the modular post-install system (sourced by setup.sh). + +register_service my-tool utilities "What it does (My Tool)" 8080 + +install_my_tool() { + require_docker || return 1 + + local DIR="$DOCKER_DIR/my-tool" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $DIR with docker-compose.yml" + return 0 + fi + + mkdir -p "$DIR" + ensure_docker_dir_ownership "$DIR" + cd "$DIR" || return 1 + + cat > docker-compose.yml << 'EOF' +name: my-tool +services: + my-tool: + image: vendor/my-tool:latest + container_name: my-tool + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - ./data:/data +EOF + + configure_caddy_for_service "My Tool" "8080" "my-tool" + + write_readme "$DIR" << 'MD' +# My Tool +Brief description. + +## Manage +```bash +docker compose up -d +docker compose down +docker compose logs -f +docker compose pull && docker compose up -d +``` +MD + + local START="" + prompt_yn "Start My Tool now? (y/n):" "y" START + if [ "$START" = "y" ] || [ "$START" = "Y" ]; then + docker compose up -d \ + && log_success "My Tool started" \ + || log_warning "Start failed — check: docker compose logs" + fi +} +``` + +## register_service signature + +```bash +register_service "" [port] +``` + +- `name` — kebab-case, matches the filename and the `install_` function +- `group` — one of the categories below; determines which menu it appears in +- `description` — shown in `--list` and the menu checklist +- `port` — optional; informational only (not used by the framework) + +## Available globals + +| Variable | Value | +|----------|-------| +| `DOCKER_DIR` | `~/docker` — parent for all Docker service directories | +| `ACTUAL_USER` | The non-root user that invoked sudo | +| `ACTUAL_HOME` | Home directory of `ACTUAL_USER` | +| `SITE_TZ` | Timezone from site config, e.g. `America/New_York` | +| `SITE_DOMAIN` | Base domain from site config, e.g. `example.com` | +| `SITE_CADDY_NET` | Docker network name for Caddy (default: `caddy_net`) | +| `DRY_RUN` | `true`/`false` — set by `--dry-run` flag | +| `UNATTENDED` | `true`/`false` — set by `--unattended` flag | + +## Available helpers (lib/common.sh) + +### Logging + +```bash +log_info "message" # blue [INFO] +log_success "message" # green [OK] +log_warning "message" # yellow [WARN] +log_error "message" # red [ERROR] +``` + +### Prompts — honor `UNATTENDED` automatically + +```bash +prompt_yn "Question? (y/n):" "default_y_or_n" VARNAME +prompt_text "Question? [default]:" "default" VARNAME +``` + +When `UNATTENDED=true` both functions skip the prompt and use the default. + +### Pre-flight + +```bash +require_root # exits with an error if not running as root +require_docker # installs Docker CE + Compose plugin if missing, then returns +``` + +### Execution and ownership + +```bash +run_cmd COMMAND [args...] # no-ops in DRY_RUN, executes otherwise +ensure_docker_dir_ownership DIR... # chown -R ACTUAL_USER:ACTUAL_USER (skips in DRY_RUN) +generate_password [length] # alphanumeric random string, default 32 chars +pip_user_install PACKAGE... # pip3 --user with --break-system-packages on 24.04+ +``` + +### Caddy reverse proxy + +```bash +configure_caddy_for_service "Display Name" "PORT" "default-subdomain" ["extra-block"] +``` + +Prompts the user for a domain, appends a site block to the Caddyfile, and +reloads Caddy. No-ops silently if Caddy isn't installed. The fourth argument +is an optional string inserted verbatim inside the Caddy site block (use it +for `import authelia` or custom matchers). + +### README generation + +```bash +write_readme "$DIR" << 'MD' +# Title +Content +MD +``` + +Writes `$DIR/README.md` (creates the directory if needed). No-ops in DRY_RUN. +Every Docker service should call this so `~/docker//README.md` is +self-documenting on the deployed box. + +## Categories + +| Group | Purpose | +|-------|---------| +| `base` | CLI packages installed on every box | +| `homelab` | Core infrastructure — reverse proxy, auth, intrusion prevention | +| `utilities` | Self-hosted web apps — budget, DNS, files, monitoring, VPN, etc. | +| `media` | Media servers, photo backup, disc ripping | +| `cameras` | NVR and camera tooling (Frigate) | +| `gaming` | Game servers, cloud gaming (Wolf), emulation | +| `extras` | Non-Docker tools and scripts | +| `backup` | Backup solutions | + +## Non-Docker services + +Not everything is a container. For apt-based or git-clone–based services, +skip `require_docker` and the Docker helpers. See `services/base.sh` (apt +packages + Charm repo) and `services/crowdsec.sh` (official apt repo) as +reference patterns. + +For non-Docker services the default `is_installed` check in `setup.sh` +looks for `$DOCKER_DIR/$name`, which won't exist. Add a case to the +`is_installed()` function in `setup.sh` so the `[installed]` marker appears +correctly in the menu: + +```bash +# In setup.sh → is_installed() +my-tool) command -v my-tool >/dev/null 2>&1 ;; +``` + +Docker services use the default case and don't need an entry. + +## DRY_RUN convention + +Every `install_*` function must check `$DRY_RUN` before touching the +filesystem, installing packages, or starting containers. The pattern is: + +```bash +if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would do X" + echo "[DRY-RUN] Would do Y" + return 0 +fi +``` + +Put the check early — after any pure-display output (banners, info text) +but before the first write. + +## .env files and secrets + +Generate passwords with `generate_password` (never hardcode them). +Write secrets to `.env` files in the service directory, owned by +`ACTUAL_USER`, permissions 600. Document every variable with a comment +in the `.env` heredoc so the user knows what to change later. + +## Caddy network wiring + +Services that need to reach Caddy (or each other) over Docker networking +should join the `$SITE_CADDY_NET` network. Add to `docker-compose.yml`: + +```yaml +networks: + caddy_net: + external: true + name: ${CADDY_NET:-caddy_net} +``` + +And read the network name from `.env` using `CADDY_NET=$SITE_CADDY_NET`. diff --git a/README.md b/README.md index af5215d..317c23e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ sudo ./setup.sh --unattended base # non-interactive, use defaults ## What the wizard does **First run:** -1. Installs essential CLI packages (see [Base packages](#base-packages) below) +1. Installs essential CLI packages (`net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`, `glow`) 2. Checks for Docker CE + Compose plugin; prompts to install if missing 3. Offers to set **site defaults** — timezone, base domain, Caddy Docker network — so every service picks them up automatically instead of asking each time @@ -72,181 +72,10 @@ Update them any time with `sudo ./setup.sh configure`. | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | | `gaming` | `js99er`, `minecraft`, `wolf`, `wolf-pair` | | `extras` | `silent-send`, `sync-cc` | -| `backup` | 'Kopia, Borg (encrypted, dedup, scheduled)', rsync (plain or with --link-dest)' rsnapshot | +| `backup` | `backup` — Kopia (built-in, encrypted, dedup, scheduled); Borg, rsync, rsnapshot also supported (see installer) | Run `./setup.sh --list` to see descriptions. -## Backup - -The `backup` service (under the `backup` group) sets up **Kopia** — but you are -not limited to it. Below is a guide to every backup strategy in this repo, -with advice on when to reach for each one. - ---- - -### Kopia — the built-in `backup` service - -**What it does:** block-level deduplication + zstd compression + AES-256 -encryption, scheduled automatically via a systemd timer (cron fallback). -Each run snapshots configured paths and retains versions according to a -policy (latest N, daily, weekly, monthly). An optional `sync-to` step -mirrors the whole encrypted repository to another computer or a cloud -bucket (SFTP, Backblaze B2, S3, rclone). - -**Install:** `sudo ./setup.sh backup` - -**When to use:** files that change *constantly* — Minecraft region files, -game saves, Steam Proton prefixes, Docker config volumes. Every changed -block is stored once; identical blocks across snapshots share storage. -Kopia is the right default for homelab data that changes on every write. - ---- - -### Borg Backup - -**What it is:** `borgbackup` — chunk-based deduplication + lz4/zstd/zlib -compression + AES-CTR encryption. Mature, battle-tested, wide ecosystem -(Borgmatic for YAML-driven automation, Vorta for a GUI). - -```bash -sudo apt install borgbackup - -# initialise a repo -borg init --encryption=repokey /backups/borg-repo - -# take a snapshot -borg create --stats --progress /backups/borg-repo::'{hostname}-{now}' ~/docker - -# list snapshots -borg list /backups/borg-repo - -# restore -borg extract /backups/borg-repo::snapshot-name -``` - -**When to use:** same class of problem as Kopia — frequently-changing files -where deduplication pays off. Choose Borg over Kopia if you prefer its -mature ecosystem, Borgmatic config files, or need to share a repo across -multiple machines. Both are excellent; pick the one whose tooling you -prefer. - ---- - -### rsync — plain mirror (no versioning) - -**What it is:** `rsync -av --delete SOURCE/ DEST/` — fast one-way mirror. -`--delete` removes files in `DEST` that no longer exist in `SOURCE`. -The destination is a plain readable copy of the source; no special tool -needed to browse or restore. - -```bash -rsync -av --delete /source/media/ /backups/media/ -``` - -**When to use:** files that rarely or never change — media libraries, -ROM collections, game installs you could re-download but prefer to keep -locally. You just need *a copy*, not versioning. rsync is lightweight, -transparent, and the destination needs no special format. - -**Not suitable for:** files that change often, because one bad `--delete` -run (source corruption, accidental deletion) immediately destroys the -only copy in the destination. - ---- - -### rsync `--link-dest` — versioned snapshots with original structure - -**What it is:** each backup run creates a new dated directory. Unchanged -files are **hard-linked** from the previous backup rather than copied, so -unchanged files cost no extra disk space. Every dated directory looks like -a complete independent snapshot of the source — original folder structure -preserved, no rsnapshot-style naming convention. - -```bash -#!/bin/bash -DEST=/backups/snapshots -PREV="$DEST/$(ls -1 "$DEST" | tail -1)" # most recent snapshot -TODAY="$DEST/$(date +%F)" - -rsync -av --delete --link-dest="$PREV" /source/ "$TODAY/" -``` - -Run this daily (via cron or a systemd timer) and you get: - -``` -/backups/snapshots/ - 2024-01-13/ ← full copy (first run) - 2024-01-14/ ← only changed files stored; rest are hard links - 2024-01-15/ ← same -``` - -If yesterday's run with `--delete` removed everything from the source -(accidental wipe, filesystem corruption), today's snapshot will also be -empty — but `2024-01-14/` and `2024-01-13/` are untouched and fully -restorable with a plain `cp -al` or `rsync`. - -**When to use:** general files where you want versioned point-in-time -backups **and** need the original folder structure preserved. No extra -tool required to restore — every dated folder is browsable with `ls` and -copyable with standard Unix tools. Simpler than Borg/Kopia; no encryption -or deduplication across snapshot boundaries. - -**Compared to rsnapshot:** rsync `--link-dest` keeps your own naming and -structure; rsnapshot imposes `daily.0/`, `daily.1/`, etc. and manages -rotation automatically. `--link-dest` gives you more control; rsnapshot -gives you easier automation. - ---- - -### rsnapshot — automated versioned snapshots - -**What it is:** a wrapper around rsync that manages hard-link snapshots -automatically using a retention scheme (`hourly.0`, `daily.0`, `weekly.0`, -…). Configure sources and retention in `/etc/rsnapshot.conf`, then run on -a schedule. - -```bash -sudo apt install rsnapshot -# edit /etc/rsnapshot.conf — set snapshot_root, backup sources, retain counts -rsnapshot daily # run manually or via cron -rsnapshot -t daily # dry-run / test config -``` - -The resulting layout looks like: - -``` -/backups/rsnapshot/ - daily.0/ ← most recent - daily.1/ - daily.2/ - weekly.0/ -``` - -Each interval directory contains a full view of the source, hard-linked -where files are unchanged. - -**When to use:** you want automated versioned backups without writing your -own `--link-dest` rotation script, and you don't mind that the destination -uses rsnapshot's own directory naming rather than your original structure. -Good for simple setups (home directories, config files) where the rotation -automation saves time. - -**Not ideal for:** frequently-changing large files (game saves, databases) -— use Kopia or Borg instead for efficient deduplication across many -changed blocks. - ---- - -### Choosing the right tool - -| Scenario | Recommended tool | -|----------|-----------------| -| Minecraft worlds, game saves, Steam prefixes — change on every write | **Kopia** (built-in) or **Borg** | -| Media library, ROMs — rarely change, just need a copy | **rsync plain** | -| General files — want versioning, want original folder structure | **rsync `--link-dest`** | -| Want automated rotation without scripting `--link-dest` yourself | **rsnapshot** | -| Multi-machine deduplicated repo, YAML-driven config (Borgmatic) | **Borg** | - ## Layout ``` @@ -254,6 +83,7 @@ setup.sh dispatcher — wizard, direct install, --list, --dry-run lib/common.sh shared helpers: logging, prompts, site config, OS detection services/ one file per service (self-registering) extras/ non-Docker assets bundled with the repo (e.g. sync_cc.py) +CLAUDE.md contributor guide — how to add services, available helpers ``` ## Managing installed services diff --git a/services/backup.sh b/services/backup.sh index 0a32e69..c167017 100644 --- a/services/backup.sh +++ b/services/backup.sh @@ -25,6 +25,68 @@ register_service backup backup "Automatic encrypted backups (Kopia)" install_backup() { log_info "Setting up automatic encrypted backups (Kopia)..." + echo "" + echo "╔═══════════════════════════════════════════════════════════════════╗" + echo "║ BACKUP STRATEGIES — choose the right tool for your data ║" + echo "╚═══════════════════════════════════════════════════════════════════╝" + echo "" + echo " This installer sets up Kopia (the recommended default), but here" + echo " is a quick guide to all available options so you can pick the" + echo " right tool for each type of data." + echo "" + echo " ┌─────────────────────────────────────────────────────────────────┐" + echo " │ KOPIA (installed here) — block-level dedup + zstd + encryption │" + echo " │ Use for: files that change constantly — Minecraft worlds, │" + echo " │ game saves, Steam prefixes, Docker config volumes. │" + echo " │ Changed blocks are stored once; old blocks are shared. │" + echo " │ Restores via: kopia snapshot list / kopia restore │" + echo " └─────────────────────────────────────────────────────────────────┘" + echo "" + echo " ┌─────────────────────────────────────────────────────────────────┐" + echo " │ BORG (sudo apt install borgbackup) — chunk dedup + encryption │" + echo " │ Use for: same as Kopia. Choose Borg if you prefer Borgmatic │" + echo " │ (YAML config), Vorta (GUI), or multi-machine repos. │" + echo " │ borg init / borg create / borg list / borg extract │" + echo " └─────────────────────────────────────────────────────────────────┘" + echo "" + echo " ┌─────────────────────────────────────────────────────────────────┐" + echo " │ RSYNC plain rsync -av --delete /src/ /dest/ │" + echo " │ Use for: media, ROMs, files that rarely change and you just │" + echo " │ need a copy. Fast, transparent — no special restore tool. │" + echo " │ Not suitable for files that change often (one bad --delete │" + echo " │ run immediately destroys the only copy in the destination). │" + echo " └─────────────────────────────────────────────────────────────────┘" + echo "" + echo " ┌─────────────────────────────────────────────────────────────────┐" + echo " │ RSYNC --link-dest versioned snapshots, original folder layout │" + echo " │ Creates dated dirs (2024-01-15/, 2024-01-16/, …). │" + echo " │ Unchanged files are hard-linked — cost no extra disk space. │" + echo " │ Each dated dir is a complete, browsable snapshot of the │" + echo " │ source. Original folder structure preserved (unlike │" + echo " │ rsnapshot). If today's --delete wiped something, yesterday's │" + echo " │ dated dir is untouched. │" + echo " │ Use for: general files where you want versioning + readable │" + echo " │ snapshot dirs without a special restore tool. │" + echo " └─────────────────────────────────────────────────────────────────┘" + echo "" + echo " ┌─────────────────────────────────────────────────────────────────┐" + echo " │ RSNAPSHOT (sudo apt install rsnapshot) — automated rotation │" + echo " │ Wraps rsync with a retention scheme (daily.0, weekly.0, …). │" + echo " │ Hard-links unchanged files like --link-dest, but dirs are │" + echo " │ named by rsnapshot (not your original structure). │" + echo " │ Use for: automated versioning without scripting --link-dest, │" + echo " │ when the rsnapshot naming convention doesn't bother you. │" + echo " └─────────────────────────────────────────────────────────────────┘" + echo "" + echo " Quick reference:" + echo " Constantly-changing data (saves, worlds, configs) → Kopia or Borg" + echo " Media / ROMs (rarely changes, just need a copy) → rsync plain" + echo " Versioned snapshots, keep original folder layout → rsync --link-dest" + echo " Versioned snapshots, want auto rotation scripted → rsnapshot" + echo "" + echo " Continuing with Kopia setup..." + echo "" + # ── Repo-conventional paths ────────────────────────────────────────────── local BACKUP_DIR="$DOCKER_DIR/backup" local CONF_FILE="$BACKUP_DIR/backup.conf" # editable settings diff --git a/services/linux-to-sync.sh b/services/linux-to-sync.sh deleted file mode 100644 index 9879964..0000000 --- a/services/linux-to-sync.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash -# services/linux-to-sync.sh — Clone the private linux-to-sync repository. -# Part of the modular post-install system (sourced by setup.sh). -# -# Ported from ubuntu-post-install-24.04-crowdsec.sh (# ---- LINUX-TO-SYNC ----). -# Clones outis1one/linux-to-sync to ~/linux-to-sync via SSH or HTTPS+PAT. -# No server/container — this is a personal sync/config repo. - -register_service linux-to-sync extras "Personal sync & config scripts (linux-to-sync private repo)" - -install_linux-to-sync() { - local SYNC_DIR="$ACTUAL_HOME/linux-to-sync" - - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] linux-to-sync would:" - echo " - Clone outis1one/linux-to-sync to $SYNC_DIR" - echo " - Authenticate via SSH key or GitHub Personal Access Token" - return 0 - fi - - # ── Re-run: already cloned → offer pull ────────────────────────────────── - if [ -d "$SYNC_DIR/.git" ]; then - log_info "linux-to-sync already cloned at $SYNC_DIR" - local DO_PULL="" - prompt_yn "Pull latest changes? (y/n) [y]:" "y" DO_PULL - if [[ ${DO_PULL:-y} =~ ^[Yy]$ ]]; then - if sudo -u "$ACTUAL_USER" git -C "$SYNC_DIR" pull; then - log_success "linux-to-sync updated" - else - log_warning "git pull failed — check connectivity and credentials" - fi - fi - return 0 - fi - - echo "" - echo " Requires access to github.com/outis1one/linux-to-sync" - echo " Authenticate with ONE of:" - echo " [1] SSH key already added to your GitHub account" - echo " [2] GitHub Personal Access Token (PAT)" - echo "" - - local AUTH_METHOD="" - prompt_text "Authentication method [1=SSH, 2=PAT, default: 1]:" "1" AUTH_METHOD - AUTH_METHOD="${AUTH_METHOD:-1}" - - if [ "$AUTH_METHOD" = "2" ]; then - echo "" - echo " Create a PAT at: https://github.com/settings/tokens/new" - echo " Select the 'repo' scope for full repository access." - echo "" - local GH_TOKEN="" - prompt_text "GitHub Personal Access Token:" "" GH_TOKEN - if [ -z "$GH_TOKEN" ]; then - log_warning "No token provided — skipping." - return 0 - fi - - log_info "Cloning via HTTPS + PAT..." - if sudo -u "$ACTUAL_USER" \ - git clone "https://$GH_TOKEN@github.com/outis1one/linux-to-sync.git" "$SYNC_DIR"; then - # Remove token from remote URL so it isn't stored in plain text - sudo -u "$ACTUAL_USER" git -C "$SYNC_DIR" remote set-url origin \ - "https://github.com/outis1one/linux-to-sync.git" - log_success "linux-to-sync cloned to $SYNC_DIR" - echo " Token stripped from remote URL. For future pulls use:" - echo " git -C $SYNC_DIR pull (will prompt for credentials)" - echo " Or set up a credential helper:" - echo " git config --global credential.helper store" - else - log_error "Clone failed — check your PAT and network, then retry." - return 1 - fi - else - # SSH auth — git must run as the actual user to use their SSH keys. - echo "" - echo " Checking for SSH key in $ACTUAL_HOME/.ssh/ ..." - local SSH_KEY_FOUND=false - for _k in id_ed25519 id_rsa id_ecdsa; do - if [ -f "$ACTUAL_HOME/.ssh/$_k" ]; then - log_info " Found: $ACTUAL_HOME/.ssh/$_k" - SSH_KEY_FOUND=true - break - fi - done - if [ "$SSH_KEY_FOUND" = false ]; then - log_warning "No SSH key found in $ACTUAL_HOME/.ssh/" - echo "" - echo " To generate one:" - echo " ssh-keygen -t ed25519 -C 'your@email.com'" - echo " cat $ACTUAL_HOME/.ssh/id_ed25519.pub" - echo " → Add the public key at: github.com/settings/keys" - echo "" - local CONTINUE="" - prompt_yn "Continue anyway (will fail if no key on GitHub)? (y/n) [n]:" "n" CONTINUE - [[ ${CONTINUE:-n} =~ ^[Yy]$ ]] || return 0 - fi - - log_info "Cloning via SSH (running as $ACTUAL_USER)..." - if sudo -u "$ACTUAL_USER" \ - git clone git@github.com:outis1one/linux-to-sync.git "$SYNC_DIR"; then - log_success "linux-to-sync cloned to $SYNC_DIR" - else - log_error "SSH clone failed." - echo "" - echo " Common causes:" - echo " • SSH key not added to GitHub — go to github.com/settings/keys" - echo " • Key not accepted by ssh-agent — try: ssh-add $ACTUAL_HOME/.ssh/id_ed25519" - echo " • Test with: sudo -u $ACTUAL_USER ssh -T git@github.com" - echo " Then retry: sudo ./setup.sh linux-to-sync" - return 1 - fi - fi - - write_readme "$SYNC_DIR" << MD -# linux-to-sync - -Private personal sync and config repository cloned from outis1one/linux-to-sync. - -## Update -\`\`\`bash -cd $SYNC_DIR -git pull -\`\`\` -MD - - echo "" - echo " Cloned to: $SYNC_DIR" - echo "" -} diff --git a/setup.sh b/setup.sh index dc082e7..27b8a7b 100755 --- a/setup.sh +++ b/setup.sh @@ -80,7 +80,6 @@ is_installed() { glow) command -v glow >/dev/null 2>&1 ;; crowdsec) command -v cscli >/dev/null 2>&1 ;; silent-send) [ -d "$ACTUAL_HOME/silent-send/.git" ] ;; - linux-to-sync) [ -d "$ACTUAL_HOME/linux-to-sync/.git" ] ;; sync-cc) [ -f "$ACTUAL_HOME/sync-cc/sync_cc.py" ] ;; sky-cam) [ -d "$ACTUAL_HOME/sky-cam/.git" ] ;; *) [ -e "$DOCKER_DIR/$1" ] ;;