chore: install.sh with --install/--update/--remove flags, help, update README
This commit is contained in:
parent
64844baa83
commit
3bc3eddd5b
2 changed files with 396 additions and 268 deletions
56
README.md
56
README.md
|
|
@ -9,13 +9,20 @@
|
||||||
## 🚀 One-Command Install
|
## 🚀 One-Command Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash -c "$(curl -fsSL https://git.lohmar.co.uk/cclohmar/NextExpense/raw/branch/main/install.sh)"
|
bash -c "$(curl -fsSL https://git.lohmar.co.uk/cclohmar/NextExpense/raw/branch/main/install.sh)" -s -- --install
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from a local clone:
|
Or from a local clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./install.sh
|
./install.sh --install
|
||||||
|
```
|
||||||
|
|
||||||
|
Running without flags shows help:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh # Show help
|
||||||
|
./install.sh --help # Same
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer will:
|
The installer will:
|
||||||
|
|
@ -41,11 +48,19 @@ The installer will:
|
||||||
## 🔄 Updating
|
## 🔄 Updating
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /opt/nextexpense/install.sh -update
|
./install.sh --update
|
||||||
```
|
```
|
||||||
|
|
||||||
Update pulls the latest code, copies updated templates/static, rebuilds the binary, automatically backs up the database, and restarts the service.
|
Update pulls the latest code, copies updated templates/static, rebuilds the binary, automatically backs up the database, and restarts the service.
|
||||||
|
|
||||||
|
## 🗑️ Uninstalling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./install.sh --remove
|
||||||
|
```
|
||||||
|
|
||||||
|
Stops the service, removes the systemd unit, and deletes `/opt/nextexpense/` (asks for confirmation).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Building from Source (install.sh does this automatically)
|
## 🔧 Building from Source (install.sh does this automatically)
|
||||||
|
|
@ -118,14 +133,14 @@ Navigate to `http://YOUR_SERVER:8080`
|
||||||
1. Enter your email → click "Send Verification Code"
|
1. Enter your email → click "Send Verification Code"
|
||||||
2. Check your inbox (or server log) for the 6-digit OTP
|
2. Check your inbox (or server log) for the 6-digit OTP
|
||||||
3. Enter OTP → click "Verify Code"
|
3. Enter OTP → click "Verify Code"
|
||||||
4. Click "+ New" → name your event → set claim currency → enter a conversion sample
|
4. Create a month (e.g. "July 2026") → click "Open"
|
||||||
5. Click "Open" on the event
|
5. Click "+ New" to add an event → set claim currency + conversion sample
|
||||||
6. Tap "📷 Camera" or "📁 Upload" to add a receipt
|
6. Click "Open" on the event
|
||||||
7. AI extracts amount, merchant, category, date → form is pre-filled
|
7. Tap "📷 Camera" or "📁 Upload" to add a receipt
|
||||||
8. Click "Save Expense"
|
8. AI extracts amount, merchant, category, date → form is pre-filled
|
||||||
9. Click "Submit Event" → enter recipient email → choose CSV or PDF
|
9. Click "Save Expense"
|
||||||
10. Report + receipt images ZIP are emailed → event auto-closes
|
10. Back on the month view, click "Generate Monthly Report"
|
||||||
11. Click "Reopen" to add more receipts
|
11. Monthly ZIP contains CSV + PDF report + all receipt images
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -145,15 +160,22 @@ Navigate to `http://YOUR_SERVER:8080`
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `GET` | `/dashboard` | Event list |
|
| `GET` | `/dashboard` | Month list |
|
||||||
| `POST` | `/events` | Create event |
|
| `POST` | `/months` | Create month |
|
||||||
| `GET` | `/events/{id}/expenses` | View event + expenses |
|
| `GET` | `/months/{mid}` | View month + events |
|
||||||
| `PUT` | `/events/{id}/reopen` | Reopen closed event |
|
| `PUT` | `/months/{mid}` | Update month |
|
||||||
|
| `DELETE` | `/months/{mid}` | Delete month |
|
||||||
|
| `POST` | `/months/{mid}/events` | Create event |
|
||||||
|
| `PUT` | `/months/{mid}/events/{eid}` | Update event |
|
||||||
|
| `DELETE` | `/months/{mid}/events/{eid}` | Delete event |
|
||||||
|
| `GET` | `/months/{mid}/events/{eid}/expenses` | View event + expenses |
|
||||||
|
| `POST` | `/months/{mid}/events/{eid}/generate` | Generate event report |
|
||||||
|
| `POST` | `/months/{mid}/generate` | Generate monthly report |
|
||||||
| `POST` | `/expenses/upload` | Upload receipt image/PDF |
|
| `POST` | `/expenses/upload` | Upload receipt image/PDF |
|
||||||
| `POST` | `/expenses` | Save expense |
|
| `POST` | `/expenses` | Save expense |
|
||||||
| `GET` | `/expenses/{id}/edit` | Get edit form |
|
| `GET` | `/expenses/{id}/edit` | Get edit form |
|
||||||
| `PUT` | `/expenses/{id}` | Update expense |
|
| `PUT` | `/expenses/{id}` | Update expense |
|
||||||
| `POST` | `/events/{id}/file` | File report (CSV/PDF + images ZIP) |
|
| `DELETE` | `/expenses/{id}` | Delete expense |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -215,7 +237,7 @@ Navigate to `http://YOUR_SERVER:8080`
|
||||||
|
|
||||||
## 🗄️ Database
|
## 🗄️ Database
|
||||||
|
|
||||||
SQLite, auto-created on first run — 4 tables: `users`, `auth_otps`, `events`, `expenses`.
|
SQLite, auto-created on first run — 5 tables: `users`, `auth_otps`, `months`, `events`, `expenses`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
608
install.sh
608
install.sh
|
|
@ -4,9 +4,13 @@
|
||||||
# =====================
|
# =====================
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# curl -fsSL https://git.lohmar.co.uk/cclohmar/NextExpense/raw/branch/main/install.sh | bash
|
# ./install.sh — Show help
|
||||||
# bash install.sh — Full install
|
# ./install.sh --install — Full fresh install
|
||||||
# bash install.sh -update — Pull latest, rebuild, restart
|
# ./install.sh --update — Pull latest, rebuild, restart
|
||||||
|
# ./install.sh --remove — Stop service, remove all files
|
||||||
|
#
|
||||||
|
# Pipe install (requires --install flag):
|
||||||
|
# curl -fsSL https://git.lohmar.co.uk/cclohmar/NextExpense/raw/branch/main/install.sh | bash -s -- --install
|
||||||
#
|
#
|
||||||
# Installs to /opt/nextexpense/ and sets up a systemd service.
|
# Installs to /opt/nextexpense/ and sets up a systemd service.
|
||||||
# Uses sudo internally only for operations that require it.
|
# Uses sudo internally only for operations that require it.
|
||||||
|
|
@ -39,22 +43,110 @@ sudo_if() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# UPDATE MODE
|
# HELP
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
if [ "${1:-}" = "-update" ]; then
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
╔═══════════════════════════════════════════╗
|
||||||
|
║ NextExpense Installer ║
|
||||||
|
╚═══════════════════════════════════════════╝
|
||||||
|
|
||||||
|
Usage: ./install.sh [FLAG]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--install, -i Full fresh install (clone, build, configure, start service)
|
||||||
|
--update, -u Pull latest, rebuild binary, restart service
|
||||||
|
--remove, -r Stop service, remove files (asks for confirmation)
|
||||||
|
--help, -h Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./install.sh --install
|
||||||
|
./install.sh --update
|
||||||
|
./install.sh --remove
|
||||||
|
|
||||||
|
Pipe install:
|
||||||
|
curl -fsSL $REPO_URL/install.sh | bash -s -- --install
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
# REMOVE / UNINSTALL
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
do_remove() {
|
||||||
echo ""
|
echo ""
|
||||||
echo " ╔═══════════════════════════════════════╗"
|
echo " ╔═══════════════════════════════════════╗"
|
||||||
echo " ║ NextExpense — Update Mode ║"
|
echo " ║ NextExpense — Uninstall ║"
|
||||||
echo " ╚═══════════════════════════════════════╝"
|
echo " ╚═══════════════════════════════════════╝"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Cache sudo credentials upfront so subsequent sudo_if calls don't prompt.
|
if [ ! -d "$INSTALL_DIR" ] && [ ! -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
|
||||||
|
warn "Nothing to remove — NextExpense is not installed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " This will:"
|
||||||
|
echo " 1. Stop the ${SERVICE_NAME} service"
|
||||||
|
echo " 2. Disable and remove the systemd service file"
|
||||||
|
echo " 3. Remove ${INSTALL_DIR}/ (including database, receipts, and config)"
|
||||||
|
echo " 4. Remove /var/log/nextexpense.log"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ask " Type 'yes' to confirm: "
|
||||||
|
read -r CONFIRM
|
||||||
|
if [ "$CONFIRM" != "yes" ]; then
|
||||||
|
warn "Aborted."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cache sudo credentials.
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
sudo -v 2>/dev/null || { err "This update requires sudo access. Please run: sudo ./install.sh -update"; exit 1; }
|
sudo -v 2>/dev/null || { err "Uninstall requires sudo access."; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop and disable service.
|
||||||
|
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
|
||||||
|
info "Stopping service..."
|
||||||
|
sudo_if systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
sudo_if systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
sudo_if rm -f "/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
sudo_if systemctl daemon-reload
|
||||||
|
info "Service removed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove install directory.
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
info "Removing $INSTALL_DIR..."
|
||||||
|
sudo_if rm -rf "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove log file.
|
||||||
|
if [ -f "/var/log/nextexpense.log" ]; then
|
||||||
|
sudo_if rm -f "/var/log/nextexpense.log"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "NextExpense has been uninstalled."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
# UPDATE
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
do_update() {
|
||||||
|
echo ""
|
||||||
|
echo " ╔═══════════════════════════════════════╗"
|
||||||
|
echo " ║ NextExpense — Update ║"
|
||||||
|
echo " ╚═══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Cache sudo credentials upfront.
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
sudo -v 2>/dev/null || { err "This update requires sudo access."; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d "$INSTALL_DIR" ]; then
|
if [ ! -d "$INSTALL_DIR" ]; then
|
||||||
err "$INSTALL_DIR does not exist. Run install.sh without -update first."
|
err "$INSTALL_DIR does not exist. Run --install first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -73,46 +165,43 @@ if [ "${1:-}" = "-update" ]; then
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Determine the app user from the existing service file (most reliable).
|
# Determine the app user.
|
||||||
APP_USER=""
|
APP_USER=""
|
||||||
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
|
if [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ]; then
|
||||||
APP_USER=$(grep -Po '^User=\K.*' "/etc/systemd/system/${SERVICE_NAME}.service" 2>/dev/null || true)
|
APP_USER=$(grep -Po '^User=\K.*' "/etc/systemd/system/${SERVICE_NAME}.service" 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
# Fallback: check the owner of the install directory or binary.
|
|
||||||
if [ -z "$APP_USER" ]; then
|
if [ -z "$APP_USER" ]; then
|
||||||
APP_USER=$(stat -c '%U' "$INSTALL_DIR/app" 2>/dev/null || stat -c '%U' "$INSTALL_DIR" 2>/dev/null || true)
|
APP_USER=$(stat -c '%U' "$INSTALL_DIR/app" 2>/dev/null || stat -c '%U' "$INSTALL_DIR" 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
if [ -z "$APP_USER" ] || [ "$APP_USER" = "root" ]; then
|
if [ -z "$APP_USER" ] || [ "$APP_USER" = "root" ]; then
|
||||||
APP_USER="${SUDO_USER:-$(whoami)}"
|
APP_USER="${SUDO_USER:-$(whoami)}"
|
||||||
fi
|
fi
|
||||||
# Reset ownership so the app user can write files.
|
|
||||||
sudo_if chown -R "${APP_USER}:${APP_USER}" "$INSTALL_DIR" 2>/dev/null || true
|
sudo_if chown -R "${APP_USER}:${APP_USER}" "$INSTALL_DIR" 2>/dev/null || true
|
||||||
# Ensure key directories have correct permissions.
|
|
||||||
sudo_if chmod 755 "$INSTALL_DIR" "$INSTALL_DIR/templates" "$INSTALL_DIR/static" 2>/dev/null || true
|
sudo_if chmod 755 "$INSTALL_DIR" "$INSTALL_DIR/templates" "$INSTALL_DIR/static" 2>/dev/null || true
|
||||||
sudo_if chmod 775 "$INSTALL_DIR/storage" 2>/dev/null || true
|
sudo_if chmod 775 "$INSTALL_DIR/storage" 2>/dev/null || true
|
||||||
|
|
||||||
# Backup the database before making any changes.
|
# Backup database.
|
||||||
if [ -f "$INSTALL_DIR/expenses.db" ]; then
|
if [ -f "$INSTALL_DIR/expenses.db" ]; then
|
||||||
BACKUP_DIR="$INSTALL_DIR/backups"
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
||||||
sudo_if mkdir -p "$BACKUP_DIR"
|
sudo_if mkdir -p "$BACKUP_DIR"
|
||||||
BACKUP_FILE="$BACKUP_DIR/expenses-$(date +%Y%m%d-%H%M%S).db"
|
BACKUP_FILE="$BACKUP_DIR/expenses-$(date +%Y%m%d-%H%M%S).db"
|
||||||
sudo_if cp "$INSTALL_DIR/expenses.db" "$BACKUP_FILE"
|
sudo_if cp "$INSTALL_DIR/expenses.db" "$BACKUP_FILE"
|
||||||
# Restore file ownership so the app user can still write.
|
|
||||||
sudo_if chown $(stat -c "%U:%G" "$INSTALL_DIR/expenses.db") "$BACKUP_FILE" 2>/dev/null || true
|
sudo_if chown $(stat -c "%U:%G" "$INSTALL_DIR/expenses.db") "$BACKUP_FILE" 2>/dev/null || true
|
||||||
info "Database backed up to $BACKUP_FILE"
|
info "Database backed up to $BACKUP_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy updated templates, static assets, and config files.
|
# Copy updated files.
|
||||||
info "Updating templates and static assets..."
|
info "Updating templates and static assets..."
|
||||||
sudo_if rsync -a --delete templates/ "$INSTALL_DIR/templates/" 2>/dev/null || true
|
sudo_if rsync -a --delete templates/ "$INSTALL_DIR/templates/" 2>/dev/null || true
|
||||||
sudo_if rsync -a --delete static/ "$INSTALL_DIR/static/" 2>/dev/null || true
|
sudo_if rsync -a --delete static/ "$INSTALL_DIR/static/" 2>/dev/null || true
|
||||||
sudo_if cp install.sh "$INSTALL_DIR/" 2>/dev/null || true
|
sudo_if cp install.sh "$INSTALL_DIR/" 2>/dev/null || true
|
||||||
|
|
||||||
# Stop service before rebuilding so the old binary can be removed.
|
# Stop service.
|
||||||
info "Stopping service..."
|
info "Stopping service..."
|
||||||
sudo_if systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
sudo_if systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
# Ensure Go is installed for building from source.
|
# Ensure Go is installed.
|
||||||
if ! command -v go &>/dev/null; then
|
if ! command -v go &>/dev/null; then
|
||||||
info "Installing Go..."
|
info "Installing Go..."
|
||||||
if command -v apt-get &>/dev/null; then
|
if command -v apt-get &>/dev/null; then
|
||||||
|
|
@ -136,7 +225,6 @@ if [ "${1:-}" = "-update" ]; then
|
||||||
sudo_if rm -f app
|
sudo_if rm -f app
|
||||||
CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" -o app .
|
CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" -o app .
|
||||||
|
|
||||||
# Fix ownership of all files (build cache may be root-owned from earlier builds).
|
|
||||||
sudo_if chown -R "${APP_USER}:${APP_USER}" "$INSTALL_DIR" 2>/dev/null || true
|
sudo_if chown -R "${APP_USER}:${APP_USER}" "$INSTALL_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
info "Starting service..."
|
info "Starting service..."
|
||||||
|
|
@ -148,185 +236,181 @@ if [ "${1:-}" = "-update" ]; then
|
||||||
else
|
else
|
||||||
warn "Service did not start. Check: systemctl status $SERVICE_NAME"
|
warn "Service did not start. Check: systemctl status $SERVICE_NAME"
|
||||||
fi
|
fi
|
||||||
exit 0
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# FRESH INSTALL
|
# FRESH INSTALL
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
echo ""
|
do_install() {
|
||||||
echo " ╔═══════════════════════════════════════╗"
|
echo ""
|
||||||
echo " ║ NextExpense Installer ║"
|
echo " ╔═══════════════════════════════════════╗"
|
||||||
echo " ╚═══════════════════════════════════════╝"
|
echo " ║ NextExpense Installer ║"
|
||||||
echo ""
|
echo " ╚═══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# Cache sudo credentials upfront.
|
# Cache sudo credentials upfront.
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
sudo -v 2>/dev/null || { err "This installer requires sudo access. Please run: sudo ./install.sh"; exit 1; }
|
sudo -v 2>/dev/null || { err "This installer requires sudo access. Please run: sudo ./install.sh --install"; exit 1; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Install dependencies ───────────────────────────────────────────
|
# ── Install dependencies ───────────────────────────────────
|
||||||
if ! command -v git &>/dev/null; then
|
if ! command -v git &>/dev/null; then
|
||||||
info "Installing git..."
|
info "Installing git..."
|
||||||
if command -v apt-get &>/dev/null; then
|
if command -v apt-get &>/dev/null; then
|
||||||
sudo_if apt-get update -qq && sudo_if apt-get install -y -qq git 2>&1 | tail -1
|
sudo_if apt-get update -qq && sudo_if apt-get install -y -qq git 2>&1 | tail -1
|
||||||
elif command -v dnf &>/dev/null; then
|
elif command -v dnf &>/dev/null; then
|
||||||
sudo_if dnf install -y git 2>&1 | tail -1
|
sudo_if dnf install -y git 2>&1 | tail -1
|
||||||
elif command -v apk &>/dev/null; then
|
elif command -v apk &>/dev/null; then
|
||||||
sudo_if apk add git 2>&1 | tail -1
|
sudo_if apk add git 2>&1 | tail -1
|
||||||
|
else
|
||||||
|
err "Please install git manually, then re-run."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! command -v curl &>/dev/null; then
|
||||||
|
info "Installing curl..."
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
sudo_if apt-get install -y -qq curl 2>&1 | tail -1
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
sudo_if dnf install -y curl 2>&1 | tail -1
|
||||||
|
elif command -v apk &>/dev/null; then
|
||||||
|
sudo_if apk add curl 2>&1 | tail -1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! command -v python3 &>/dev/null; then
|
||||||
|
info "Installing python3..."
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
sudo_if apt-get install -y -qq python3 2>&1 | tail -1
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
sudo_if dnf install -y python3 2>&1 | tail -1
|
||||||
|
elif command -v apk &>/dev/null; then
|
||||||
|
sudo_if apk add python3 2>&1 | tail -1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Clone repo ────────────────────────────────────────────
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
warn "$INSTALL_DIR already exists — pulling latest..."
|
||||||
|
sudo_if git config --global --add safe.directory "$INSTALL_DIR" 2>/dev/null || true
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
sudo_if git pull
|
||||||
else
|
else
|
||||||
err "Please install git manually, then re-run install.sh"
|
info "Cloning repository to $INSTALL_DIR..."
|
||||||
exit 1
|
sudo_if git clone "$REPO_URL" "$INSTALL_DIR"
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
fi
|
fi
|
||||||
fi
|
sudo_if chown -R "$(whoami):$(whoami)" "$INSTALL_DIR" 2>/dev/null || true
|
||||||
if ! command -v curl &>/dev/null; then
|
|
||||||
info "Installing curl..."
|
# ── Build binary ──────────────────────────────────────────
|
||||||
if command -v apt-get &>/dev/null; then
|
ARCH="$(uname -m)"
|
||||||
sudo_if apt-get install -y -qq curl 2>&1 | tail -1
|
case "$ARCH" in
|
||||||
elif command -v dnf &>/dev/null; then
|
x86_64) ARCH="amd64" ;;
|
||||||
sudo_if dnf install -y curl 2>&1 | tail -1
|
aarch64) ARCH="arm64" ;;
|
||||||
elif command -v apk &>/dev/null; then
|
*) err "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||||
sudo_if apk add curl 2>&1 | tail -1
|
esac
|
||||||
|
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
info "Installing Go..."
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
sudo_if apt-get update -qq && sudo_if apt-get install -y -qq golang-go 2>&1 | tail -1
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
sudo_if dnf install -y golang 2>&1 | tail -1
|
||||||
|
elif command -v apk &>/dev/null; then
|
||||||
|
sudo_if apk add go 2>&1 | tail -1
|
||||||
|
else
|
||||||
|
err "No package manager found. Please install Go 1.23+ manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
# python3 is used for parsing release JSON
|
info "Building binary from source (pure Go, no CGO)..."
|
||||||
if ! command -v python3 &>/dev/null; then
|
export GOMODCACHE="${INSTALL_DIR}/.go/mod"
|
||||||
info "Installing python3..."
|
export GOPATH="${INSTALL_DIR}/.go"
|
||||||
if command -v apt-get &>/dev/null; then
|
export GOCACHE="${INSTALL_DIR}/.go/build"
|
||||||
sudo_if apt-get install -y -qq python3 2>&1 | tail -1
|
mkdir -p "${GOMODCACHE}" "${GOCACHE}" 2>/dev/null || sudo_if mkdir -p "${GOMODCACHE}" "${GOCACHE}"
|
||||||
elif command -v dnf &>/dev/null; then
|
sudo_if chown -R "$(whoami):$(whoami)" "${INSTALL_DIR}/.go" "${INSTALL_DIR}/app" 2>/dev/null || true
|
||||||
sudo_if dnf install -y python3 2>&1 | tail -1
|
sudo_if rm -f app
|
||||||
elif command -v apk &>/dev/null; then
|
CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" -o app .
|
||||||
sudo_if apk add python3 2>&1 | tail -1
|
info "Binary ready: $INSTALL_DIR/app"
|
||||||
|
|
||||||
|
# ── Create runtime directories ─────────────────────────────
|
||||||
|
sudo_if mkdir -p storage
|
||||||
|
sudo_if chmod 755 templates static storage
|
||||||
|
|
||||||
|
# ── AI Provider ────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo " ── AI Provider ──"
|
||||||
|
echo " Which AI should process receipt images?"
|
||||||
|
echo " 1) Google Gemini (cloud API, needs API key)"
|
||||||
|
echo " 2) OpenAI / Compatible (also works with Ollama, LocalAI, etc.)"
|
||||||
|
echo ""
|
||||||
|
ask " Choose [1-2] (default: 1): "
|
||||||
|
read -r AI_CHOICE
|
||||||
|
AI_CHOICE="${AI_CHOICE:-1}"
|
||||||
|
|
||||||
|
case "$AI_CHOICE" in
|
||||||
|
2)
|
||||||
|
AI_PROVIDER="openai"
|
||||||
|
ask " API key (or press Enter for Ollama): "
|
||||||
|
read -r OPENAI_API_KEY
|
||||||
|
ask " Model [gpt-4o-mini]: "
|
||||||
|
read -r AI_MODEL
|
||||||
|
AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
|
||||||
|
ask " Base URL [https://api.openai.com/v1]: "
|
||||||
|
read -r AI_BASE_URL
|
||||||
|
AI_BASE_URL="${AI_BASE_URL:-https://api.openai.com/v1}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
AI_PROVIDER="gemini"
|
||||||
|
ask " Gemini API key: "
|
||||||
|
read -r GEMINI_API_KEY
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── SMTP Configuration ────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo " ── Email (SMTP) ──"
|
||||||
|
echo " Required for sending OTP codes and expense reports."
|
||||||
|
echo " Leave blank to skip (OTP codes will be logged to console)."
|
||||||
|
echo ""
|
||||||
|
ask " SMTP host: "
|
||||||
|
read -r SMTP_HOST
|
||||||
|
if [ -n "$SMTP_HOST" ]; then
|
||||||
|
ask " SMTP port [587]: "
|
||||||
|
read -r SMTP_PORT
|
||||||
|
SMTP_PORT="${SMTP_PORT:-587}"
|
||||||
|
ask " SMTP user: "
|
||||||
|
read -r SMTP_USER
|
||||||
|
ask " SMTP password: "
|
||||||
|
read -r SMTP_PASS
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 1. Clone repo ────────────────────────────────────────────────
|
# ── General settings ──────────────────────────────────────
|
||||||
if [ -d "$INSTALL_DIR" ]; then
|
echo ""
|
||||||
warn "$INSTALL_DIR already exists — pulling latest..."
|
echo " ── General ──"
|
||||||
# Mark the repo as safe — it was created by root via sudo
|
echo " If the app is behind a proxy (e.g. dev.lohmar.co.uk), enter the domain."
|
||||||
sudo_if git config --global --add safe.directory "$INSTALL_DIR" 2>/dev/null || true
|
echo " Otherwise leave blank to use localhost."
|
||||||
cd "$INSTALL_DIR"
|
echo ""
|
||||||
sudo_if git pull
|
ask " Domain name [localhost]: "
|
||||||
else
|
read -r DOMAIN
|
||||||
info "Cloning repository to $INSTALL_DIR..."
|
DOMAIN="${DOMAIN:-localhost}"
|
||||||
sudo_if git clone "$REPO_URL" "$INSTALL_DIR"
|
ask " HTTPS? (y/N): "
|
||||||
cd "$INSTALL_DIR"
|
read -r USE_HTTPS
|
||||||
fi
|
if [[ "$USE_HTTPS" =~ ^[Yy]$ ]]; then
|
||||||
# Fix ownership so the build/pull steps can write as the invoking user
|
BASE_URL="https://${DOMAIN}"
|
||||||
sudo_if chown -R "$(whoami):$(whoami)" "$INSTALL_DIR" 2>/dev/null || true
|
|
||||||
|
|
||||||
# ── 2. Build or download binary ──────────────────────────────────
|
|
||||||
ARCH="$(uname -m)"
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64) ARCH="amd64" ;;
|
|
||||||
aarch64) ARCH="arm64" ;;
|
|
||||||
*) err "Unsupported architecture: $ARCH"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Ensure Go is installed for building from source.
|
|
||||||
if ! command -v go &>/dev/null; then
|
|
||||||
info "Installing Go..."
|
|
||||||
if command -v apt-get &>/dev/null; then
|
|
||||||
sudo_if apt-get update -qq && sudo_if apt-get install -y -qq golang-go 2>&1 | tail -1
|
|
||||||
elif command -v dnf &>/dev/null; then
|
|
||||||
sudo_if dnf install -y golang 2>&1 | tail -1
|
|
||||||
elif command -v apk &>/dev/null; then
|
|
||||||
sudo_if apk add go 2>&1 | tail -1
|
|
||||||
else
|
else
|
||||||
err "No package manager found. Please install Go 1.23+ manually."
|
BASE_URL="http://${DOMAIN}"
|
||||||
exit 1
|
fi
|
||||||
|
ask " Web server port [8080]: "
|
||||||
|
read -r PORT
|
||||||
|
PORT="${PORT:-8080}"
|
||||||
|
if [ "$DOMAIN" = "localhost" ]; then
|
||||||
|
BASE_URL="http://localhost:${PORT}"
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
info "Building binary from source (pure Go, no CGO)..."
|
# ── Write .env ─────────────────────────────────────────────
|
||||||
export GOMODCACHE="${INSTALL_DIR}/.go/mod"
|
info "Creating .env..."
|
||||||
export GOPATH="${INSTALL_DIR}/.go"
|
sudo_if tee "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
||||||
export GOCACHE="${INSTALL_DIR}/.go/build"
|
|
||||||
mkdir -p "${GOMODCACHE}" "${GOCACHE}" 2>/dev/null || sudo_if mkdir -p "${GOMODCACHE}" "${GOCACHE}"
|
|
||||||
sudo_if chown -R "$(whoami):$(whoami)" "${INSTALL_DIR}/.go" "${INSTALL_DIR}/app" 2>/dev/null || true
|
|
||||||
sudo_if rm -f app
|
|
||||||
CGO_ENABLED=0 go build -buildvcs=false -ldflags="-s -w" -o app .
|
|
||||||
info "Binary ready: $INSTALL_DIR/app"
|
|
||||||
|
|
||||||
# ── 3. Create runtime directories ────────────────────────────────
|
|
||||||
sudo_if mkdir -p storage
|
|
||||||
sudo_if chmod 755 templates static storage
|
|
||||||
|
|
||||||
# ── 4. AI Provider ───────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo " ── AI Provider ──"
|
|
||||||
echo " Which AI should process receipt images?"
|
|
||||||
echo " 1) Google Gemini (cloud API, needs API key)"
|
|
||||||
echo " 2) OpenAI / Compatible (also works with Ollama, LocalAI, etc.)"
|
|
||||||
echo ""
|
|
||||||
ask " Choose [1-2] (default: 1): "
|
|
||||||
read -r AI_CHOICE
|
|
||||||
AI_CHOICE="${AI_CHOICE:-1}"
|
|
||||||
|
|
||||||
case "$AI_CHOICE" in
|
|
||||||
2)
|
|
||||||
AI_PROVIDER="openai"
|
|
||||||
ask " API key (or press Enter for Ollama): "
|
|
||||||
read -r OPENAI_API_KEY
|
|
||||||
ask " Model [gpt-4o-mini]: "
|
|
||||||
read -r AI_MODEL
|
|
||||||
AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
|
|
||||||
ask " Base URL [https://api.openai.com/v1]: "
|
|
||||||
read -r AI_BASE_URL
|
|
||||||
AI_BASE_URL="${AI_BASE_URL:-https://api.openai.com/v1}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
AI_PROVIDER="gemini"
|
|
||||||
ask " Gemini API key: "
|
|
||||||
read -r GEMINI_API_KEY
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# ── 5. SMTP Configuration ────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo " ── Email (SMTP) ──"
|
|
||||||
echo " Required for sending OTP codes and expense reports."
|
|
||||||
echo " Leave blank to skip (OTP codes will be logged to console)."
|
|
||||||
echo ""
|
|
||||||
ask " SMTP host: "
|
|
||||||
read -r SMTP_HOST
|
|
||||||
if [ -n "$SMTP_HOST" ]; then
|
|
||||||
ask " SMTP port [587]: "
|
|
||||||
read -r SMTP_PORT
|
|
||||||
SMTP_PORT="${SMTP_PORT:-587}"
|
|
||||||
ask " SMTP user: "
|
|
||||||
read -r SMTP_USER
|
|
||||||
ask " SMTP password: "
|
|
||||||
read -r SMTP_PASS
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 6. General settings ──────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
echo " ── General ──"
|
|
||||||
echo " If the app is behind a proxy (e.g. dev.lohmar.co.uk), enter the domain."
|
|
||||||
echo " Otherwise leave blank to use localhost."
|
|
||||||
echo ""
|
|
||||||
ask " Domain name [localhost]: "
|
|
||||||
read -r DOMAIN
|
|
||||||
DOMAIN="${DOMAIN:-localhost}"
|
|
||||||
ask " HTTPS? (y/N): "
|
|
||||||
read -r USE_HTTPS
|
|
||||||
if [[ "$USE_HTTPS" =~ ^[Yy]$ ]]; then
|
|
||||||
BASE_URL="https://${DOMAIN}"
|
|
||||||
else
|
|
||||||
BASE_URL="http://${DOMAIN}"
|
|
||||||
fi
|
|
||||||
ask " Web server port [8080]: "
|
|
||||||
read -r PORT
|
|
||||||
PORT="${PORT:-8080}"
|
|
||||||
if [ "$DOMAIN" = "localhost" ]; then
|
|
||||||
BASE_URL="http://localhost:${PORT}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 7. Write .env ────────────────────────────────────────────────
|
|
||||||
info "Creating .env..."
|
|
||||||
sudo_if tee "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
|
||||||
# NextExpense Configuration
|
# NextExpense Configuration
|
||||||
# Generated by install.sh on $(date)
|
# Generated by install.sh on $(date)
|
||||||
|
|
||||||
|
|
@ -335,56 +419,53 @@ BASE_URL=${BASE_URL}
|
||||||
AI_PROVIDER=${AI_PROVIDER}
|
AI_PROVIDER=${AI_PROVIDER}
|
||||||
ENVEOF
|
ENVEOF
|
||||||
|
|
||||||
case "$AI_PROVIDER" in
|
case "$AI_PROVIDER" in
|
||||||
openai)
|
openai)
|
||||||
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
||||||
OPENAI_API_KEY=${OPENAI_API_KEY}
|
OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||||
AI_MODEL=${AI_MODEL}
|
AI_MODEL=${AI_MODEL}
|
||||||
AI_BASE_URL=${AI_BASE_URL}
|
AI_BASE_URL=${AI_BASE_URL}
|
||||||
ENVEOF
|
ENVEOF
|
||||||
;;
|
;;
|
||||||
gemini)
|
gemini)
|
||||||
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
||||||
GEMINI_API_KEY=${GEMINI_API_KEY}
|
GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||||
ENVEOF
|
ENVEOF
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ -n "$SMTP_HOST" ]; then
|
if [ -n "$SMTP_HOST" ]; then
|
||||||
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
sudo_if tee -a "$INSTALL_DIR/.env" > /dev/null << ENVEOF
|
||||||
SMTP_HOST=${SMTP_HOST}
|
SMTP_HOST=${SMTP_HOST}
|
||||||
SMTP_PORT=${SMTP_PORT}
|
SMTP_PORT=${SMTP_PORT}
|
||||||
SMTP_USER=${SMTP_USER}
|
SMTP_USER=${SMTP_USER}
|
||||||
SMTP_PASS=${SMTP_PASS}
|
SMTP_PASS=${SMTP_PASS}
|
||||||
ENVEOF
|
ENVEOF
|
||||||
fi
|
|
||||||
|
|
||||||
sudo_if chmod 600 "$INSTALL_DIR/.env"
|
|
||||||
info "Configuration saved to $INSTALL_DIR/.env"
|
|
||||||
|
|
||||||
# ── 8. Determine app user ──────────────────────────────────────────
|
|
||||||
# Detect the real user (the one who invoked the script, not root via sudo)
|
|
||||||
APP_USER="${SUDO_USER:-$(whoami)}"
|
|
||||||
# If still root, create a dedicated system user
|
|
||||||
if [ "$APP_USER" = "root" ]; then
|
|
||||||
APP_USER="${SERVICE_NAME}"
|
|
||||||
if ! id -u "${APP_USER}" &>/dev/null 2>&1; then
|
|
||||||
info "Creating system user '${APP_USER}'..."
|
|
||||||
sudo_if useradd --system --no-create-home --home-dir "${INSTALL_DIR}" --shell /usr/sbin/nologin "${APP_USER}"
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
info "Using existing user '${APP_USER}'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── 9. Set ownership ───────────────────────────────────────────────
|
sudo_if chmod 600 "$INSTALL_DIR/.env"
|
||||||
sudo_if chown -R "${APP_USER}:${APP_USER}" "${INSTALL_DIR}"
|
info "Configuration saved to $INSTALL_DIR/.env"
|
||||||
sudo_if chmod 755 "${INSTALL_DIR}" "${INSTALL_DIR}/templates" "${INSTALL_DIR}/static"
|
|
||||||
# Storage needs write permission for uploaded receipts
|
|
||||||
sudo_if chmod 775 "${INSTALL_DIR}/storage"
|
|
||||||
|
|
||||||
# ── 10. Systemd service ────────────────────────────────────────────
|
# ── Determine app user ────────────────────────────────────
|
||||||
info "Creating systemd service..."
|
APP_USER="${SUDO_USER:-$(whoami)}"
|
||||||
sudo_if tee "/etc/systemd/system/${SERVICE_NAME}.service" > /dev/null << SERVEOF
|
if [ "$APP_USER" = "root" ]; then
|
||||||
|
APP_USER="${SERVICE_NAME}"
|
||||||
|
if ! id -u "${APP_USER}" &>/dev/null 2>&1; then
|
||||||
|
info "Creating system user '${APP_USER}'..."
|
||||||
|
sudo_if useradd --system --no-create-home --home-dir "${INSTALL_DIR}" --shell /usr/sbin/nologin "${APP_USER}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "Using existing user '${APP_USER}'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Set ownership ─────────────────────────────────────────
|
||||||
|
sudo_if chown -R "${APP_USER}:${APP_USER}" "${INSTALL_DIR}"
|
||||||
|
sudo_if chmod 755 "${INSTALL_DIR}" "${INSTALL_DIR}/templates" "${INSTALL_DIR}/static"
|
||||||
|
sudo_if chmod 775 "${INSTALL_DIR}/storage"
|
||||||
|
|
||||||
|
# ── Systemd service ───────────────────────────────────────
|
||||||
|
info "Creating systemd service..."
|
||||||
|
sudo_if tee "/etc/systemd/system/${SERVICE_NAME}.service" > /dev/null << SERVEOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=NextExpense — AI-Powered Expense Tracker
|
Description=NextExpense — AI-Powered Expense Tracker
|
||||||
Documentation=https://git.lohmar.co.uk/cclohmar/NextExpense
|
Documentation=https://git.lohmar.co.uk/cclohmar/NextExpense
|
||||||
|
|
@ -406,39 +487,64 @@ StandardError=append:/var/log/nextexpense.log
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
SERVEOF
|
SERVEOF
|
||||||
|
|
||||||
sudo_if systemctl daemon-reload
|
sudo_if systemctl daemon-reload
|
||||||
sudo_if systemctl enable "${SERVICE_NAME}"
|
sudo_if systemctl enable "${SERVICE_NAME}"
|
||||||
info "Service created: /etc/systemd/system/${SERVICE_NAME}.service"
|
info "Service created: /etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
|
||||||
# ── 9. Start ─────────────────────────────────────────────────────
|
# ── Start ──────────────────────────────────────────────────
|
||||||
info "Starting NextExpense..."
|
info "Starting NextExpense..."
|
||||||
sudo_if systemctl restart "${SERVICE_NAME}" 2>/dev/null || true
|
sudo_if systemctl restart "${SERVICE_NAME}" 2>/dev/null || true
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " ╔═══════════════════════════════════════╗"
|
echo " ╔═══════════════════════════════════════╗"
|
||||||
echo " ║ Installation Complete! ║"
|
echo " ║ Installation Complete! ║"
|
||||||
echo " ╚═══════════════════════════════════════╝"
|
echo " ╚═══════════════════════════════════════╝"
|
||||||
echo ""
|
echo ""
|
||||||
echo " Install: $INSTALL_DIR"
|
echo " Install: $INSTALL_DIR"
|
||||||
echo " Binary: $INSTALL_DIR/app"
|
echo " Binary: $INSTALL_DIR/app"
|
||||||
echo " Config: $INSTALL_DIR/.env"
|
echo " Config: $INSTALL_DIR/.env"
|
||||||
echo " Templates: $INSTALL_DIR/templates/"
|
echo " Templates: $INSTALL_DIR/templates/"
|
||||||
echo " Static: $INSTALL_DIR/static/"
|
echo " Static: $INSTALL_DIR/static/"
|
||||||
echo " Storage: $INSTALL_DIR/storage/"
|
echo " Storage: $INSTALL_DIR/storage/"
|
||||||
echo " Service: systemctl status $SERVICE_NAME"
|
echo " Service: systemctl status $SERVICE_NAME"
|
||||||
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
echo " Logs: journalctl -u $SERVICE_NAME -f"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
||||||
info "NextExpense is running on port ${PORT}"
|
info "NextExpense is running on port ${PORT}"
|
||||||
echo " Open http://localhost:${PORT} (or your server IP)"
|
echo " Open http://localhost:${PORT} (or your server IP)"
|
||||||
else
|
else
|
||||||
warn "Service did not start. Check: systemctl status ${SERVICE_NAME}"
|
warn "Service did not start. Check: systemctl status ${SERVICE_NAME}"
|
||||||
journalctl -u "${SERVICE_NAME}" -n 20 --no-pager
|
journalctl -u "${SERVICE_NAME}" -n 20 --no-pager
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " ── Update ──"
|
echo " ── Commands ──"
|
||||||
echo " To update later: bash install.sh -update"
|
echo " Update: ./install.sh --update"
|
||||||
echo ""
|
echo " Remove: ./install.sh --remove"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
# MAIN — Parse flags
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
MODE="${1:-}"
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
--install|-i) do_install ;;
|
||||||
|
--update|-u) do_update ;;
|
||||||
|
--remove|-r) do_remove ;;
|
||||||
|
--help|-h) show_help ;;
|
||||||
|
"") show_help ;;
|
||||||
|
*)
|
||||||
|
# Backward-compat: bare `-update` still works
|
||||||
|
if [ "$MODE" = "-update" ]; then
|
||||||
|
do_update
|
||||||
|
else
|
||||||
|
err "Unknown flag: $MODE"
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue