infra: moved makefile logic to python and added .env setup
This commit is contained in:
401
Makefile
401
Makefile
@@ -1,245 +1,155 @@
|
|||||||
.POSIX:
|
|
||||||
.SUFFIXES:
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
# --- SETTINGS ---
|
# --- Config ---
|
||||||
IMAGE_NAME = alfred_media_organizer
|
export IMAGE_NAME := alfred_media_organizer
|
||||||
# renovate: datasource=docker depName=python
|
export LIBRECHAT_VERSION := v0.8.1
|
||||||
PYTHON_VERSION = $(shell grep "python" pyproject.toml | head -n 1 | sed -E 's/.*[=<>^~"]+ *([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/')
|
export PYTHON_VERSION := 3.14.2
|
||||||
PYTHON_VERSION_SHORT = $(shell echo $(PYTHON_VERSION) | cut -d. -f1,2)
|
export PYTHON_VERSION_SHORT := 3.14
|
||||||
# Change to 'uv' when ready.
|
export RAG_VERSION := v0.7.0
|
||||||
RUNNER ?= poetry
|
export RUNNER := poetry
|
||||||
SERVICE_NAME = alfred
|
export SERVICE_NAME := alfred
|
||||||
|
|
||||||
export IMAGE_NAME
|
# --- Commands ---
|
||||||
export PYTHON_VERSION
|
CLI := python3 cli.py
|
||||||
export PYTHON_VERSION_SHORT
|
DOCKER_COMPOSE := docker compose
|
||||||
export RUNNER
|
DOCKER_BUILD := docker build \
|
||||||
|
--build-arg PYTHON_VERSION=$(PYTHON_VERSION) \
|
||||||
|
--build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \
|
||||||
|
--build-arg RUNNER=$(RUNNER)
|
||||||
|
|
||||||
# --- ADAPTERS ---
|
# --- Phony ---
|
||||||
# UV uses "sync", Poetry uses "install". Both install DEV deps by default.
|
.PHONY: setup status check
|
||||||
INSTALL_CMD = $(if $(filter uv,$(RUNNER)),sync,install)
|
.PHONY: up down restart logs ps shell
|
||||||
|
.PHONY: build build-test
|
||||||
|
.PHONY: install update install-hooks
|
||||||
|
.PHONY: test coverage lint format clean prune
|
||||||
|
.PHONY: major minor patch
|
||||||
|
.PHONY: help
|
||||||
|
|
||||||
# --- MACROS ---
|
# --- Setup ---
|
||||||
ARGS = $(filter-out $@,$(MAKECMDGOALS))
|
setup:
|
||||||
BUMP_CMD = $(RUNNER) run bump-my-version bump
|
@echo "Initializing environment..."
|
||||||
COMPOSE_CMD = docker-compose
|
@$(CLI) setup \
|
||||||
DOCKER_CMD = docker build \
|
&& echo "✓ Environment ready" \
|
||||||
--build-arg PYTHON_VERSION=$(PYTHON_VERSION) \
|
|| (echo "✗ Setup failed" && exit 1)
|
||||||
--build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \
|
|
||||||
--build-arg RUNNER=$(RUNNER) \
|
|
||||||
-t $(IMAGE_NAME):latest .
|
|
||||||
|
|
||||||
RUNNER_ADD = $(RUNNER) add
|
status:
|
||||||
RUNNER_HOOKS = $(RUNNER) run pre-commit install -c ../.pre-commit-config.yaml
|
@$(CLI) status
|
||||||
RUNNER_INSTALL = $(RUNNER) $(INSTALL_CMD)
|
|
||||||
RUNNER_RUN = $(RUNNER) run
|
|
||||||
RUNNER_UPDATE = $(RUNNER) update
|
|
||||||
|
|
||||||
# --- STYLES ---
|
check:
|
||||||
B = \033[1m
|
@$(CLI) check
|
||||||
G = \033[32m
|
|
||||||
T = \033[36m
|
|
||||||
R = \033[0m
|
|
||||||
|
|
||||||
# --- TARGETS ---
|
# --- Docker ---
|
||||||
.PHONY: add build build-test check-docker check-runner clean coverage down format help init-dotenv install install-hooks lint logs major minor patch prune ps python-version restart run shell test up update _check_branch _ci-dump-config _ci-run-tests _push_tag
|
up: check
|
||||||
|
@echo "Starting containers..."
|
||||||
|
@$(DOCKER_COMPOSE) up -d --remove-orphans \
|
||||||
|
&& echo "✓ Containers started" \
|
||||||
|
|| (echo "✗ Failed to start containers" && exit 1)
|
||||||
|
|
||||||
# Catch-all for args
|
down:
|
||||||
%:
|
@echo "Stopping containers..."
|
||||||
@:
|
@$(DOCKER_COMPOSE) down \
|
||||||
|
&& echo "✓ Containers stopped" \
|
||||||
|
|| (echo "✗ Failed to stop containers" && exit 1)
|
||||||
|
|
||||||
add: check-runner
|
restart:
|
||||||
@echo "$(T)➕ Adding dependency ($(RUNNER)): $(ARGS)$(R)"
|
@echo "Restarting containers..."
|
||||||
$(RUNNER_ADD) $(ARGS)
|
@$(DOCKER_COMPOSE) restart \
|
||||||
|
&& echo "✓ Containers restarted" \
|
||||||
|
|| (echo "✗ Failed to restart containers" && exit 1)
|
||||||
|
|
||||||
build: check-docker
|
logs:
|
||||||
@echo "$(T)🐳 Building Docker image...$(R)"
|
@echo "Following logs (Ctrl+C to exit)..."
|
||||||
$(DOCKER_CMD)
|
@$(DOCKER_COMPOSE) logs -f
|
||||||
@echo "✅ Image $(IMAGE_NAME):latest ready."
|
|
||||||
|
|
||||||
build-test: check-docker
|
ps:
|
||||||
@echo "$(T)🐳 Building test image (with dev deps)...$(R)"
|
@echo "Container status:"
|
||||||
docker build \
|
@$(DOCKER_COMPOSE) ps
|
||||||
--build-arg RUNNER=$(RUNNER) \
|
|
||||||
--build-arg PYTHON_VERSION=$(PYTHON_VERSION) \
|
|
||||||
--build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \
|
|
||||||
--target test \
|
|
||||||
-t $(IMAGE_NAME):test .
|
|
||||||
@echo "✅ Test image $(IMAGE_NAME):test ready."
|
|
||||||
|
|
||||||
check-docker:
|
shell:
|
||||||
@command -v docker >/dev/null 2>&1 || { echo "$(R)❌ Docker not installed$(R)"; exit 1; }
|
@echo "Opening shell in $(SERVICE_NAME)..."
|
||||||
@docker info >/dev/null 2>&1 || { echo "$(R)❌ Docker daemon not running$(R)"; exit 1; }
|
@$(DOCKER_COMPOSE) exec $(SERVICE_NAME) /bin/bash
|
||||||
|
|
||||||
check-runner:
|
# --- Build ---
|
||||||
@command -v $(RUNNER) >/dev/null 2>&1 || { echo "$(R)❌ $(RUNNER) not installed$(R)"; exit 1; }
|
build: check
|
||||||
|
@echo "Building image $(IMAGE_NAME):latest ..."
|
||||||
|
@$(DOCKER_BUILD) -t $(IMAGE_NAME):latest . \
|
||||||
|
&& echo "✓ Build complete" \
|
||||||
|
|| (echo "✗ Build failed" && exit 1)
|
||||||
|
|
||||||
|
build-test: check
|
||||||
|
@echo "Building test image $(IMAGE_NAME):test..."
|
||||||
|
@$(DOCKER_BUILD) --target test -t $(IMAGE_NAME):test . \
|
||||||
|
&& echo "✓ Test image built" \
|
||||||
|
|| (echo "✗ Build failed" && exit 1)
|
||||||
|
|
||||||
|
# --- Dependencies ---
|
||||||
|
install:
|
||||||
|
@echo "Installing dependencies with $(RUNNER)..."
|
||||||
|
@$(RUNNER) install \
|
||||||
|
&& echo "✓ Dependencies installed" \
|
||||||
|
|| (echo "✗ Installation failed" && exit 1)
|
||||||
|
|
||||||
|
update:
|
||||||
|
@echo "Updating dependencies with $(RUNNER)..."
|
||||||
|
@$(RUNNER) update \
|
||||||
|
&& echo "✓ Dependencies updated" \
|
||||||
|
|| (echo "✗ Update failed" && exit 1)
|
||||||
|
|
||||||
|
install-hooks:
|
||||||
|
@echo "Installing pre-commit hooks..."
|
||||||
|
@$(RUNNER) run pre-commit install \
|
||||||
|
&& echo "✓ Hooks installed" \
|
||||||
|
|| (echo "✗ Hook installation failed" && exit 1)
|
||||||
|
|
||||||
|
# --- Quality ---
|
||||||
|
test:
|
||||||
|
@echo "Running tests..."
|
||||||
|
@$(RUNNER) run pytest \
|
||||||
|
&& echo "✓ Tests passed" \
|
||||||
|
|| (echo "✗ Tests failed" && exit 1)
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
@echo "Running tests with coverage..."
|
||||||
|
@$(RUNNER) run pytest --cov=. --cov-report=html --cov-report=term \
|
||||||
|
&& echo "✓ Coverage report generated" \
|
||||||
|
|| (echo "✗ Coverage failed" && exit 1)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@echo "Linting code..."
|
||||||
|
@$(RUNNER) run ruff check --fix . \
|
||||||
|
&& echo "✓ Linting complete" \
|
||||||
|
|| (echo "✗ Linting failed" && exit 1)
|
||||||
|
|
||||||
|
format:
|
||||||
|
@echo "Formatting code..."
|
||||||
|
@$(RUNNER) run ruff format . && $(RUNNER) run ruff check --fix . \
|
||||||
|
&& echo "✓ Code formatted" \
|
||||||
|
|| (echo "✗ Formatting failed" && exit 1)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "$(T)🧹 Cleaning caches...$(R)"
|
@echo "Cleaning build artifacts..."
|
||||||
rm -rf .ruff_cache __pycache__ .pytest_cache
|
@rm -rf .ruff_cache __pycache__ .pytest_cache htmlcov .coverage
|
||||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
@find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
@echo "✓ Cleanup complete"
|
||||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
|
||||||
@echo "✅ Caches cleaned."
|
|
||||||
|
|
||||||
coverage: check-runner
|
prune:
|
||||||
@echo "$(T)📊 Running tests with coverage...$(R)"
|
@echo "Pruning Docker system..."
|
||||||
$(RUNNER_RUN) pytest --cov=. --cov-report=html --cov-report=term $(ARGS)
|
@docker system prune -af \
|
||||||
@echo "✅ Report generated in htmlcov/"
|
&& echo "✓ Docker pruned" \
|
||||||
|
|| (echo "✗ Prune failed" && exit 1)
|
||||||
|
|
||||||
down: check-docker
|
# --- Versioning ---
|
||||||
@echo "$(T)🛑 Stopping containers...$(R)"
|
major minor patch: _check-main
|
||||||
$(COMPOSE_CMD) down
|
@echo "Bumping $@ version..."
|
||||||
@echo "✅ System stopped."
|
@$(RUNNER) run bump-my-version bump $@ \
|
||||||
|
&& echo "✓ Version bumped" \
|
||||||
|
|| (echo "✗ Version bump failed" && exit 1)
|
||||||
|
|
||||||
format: check-runner
|
@echo "Pushing tags..."
|
||||||
@echo "$(T)✨ Formatting with Ruff...$(R)"
|
@git push --tags \
|
||||||
$(RUNNER_RUN) ruff format .
|
&& echo "✓ Tags pushed" \
|
||||||
$(RUNNER_RUN) ruff check --fix .
|
|| (echo "✗ Push failed" && exit 1)
|
||||||
@echo "✅ Code cleaned."
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "$(B)Available commands:$(R)"
|
|
||||||
@echo ""
|
|
||||||
@echo "$(G)Setup:$(R)"
|
|
||||||
@echo " $(T)check-docker $(R) Verify Docker is installed and running."
|
|
||||||
@echo " $(T)check-runner $(R) Verify package manager ($(RUNNER))."
|
|
||||||
@echo " $(T)init-dotenv $(R) Create .env from .env.example with generated secrets."
|
|
||||||
@echo " $(T)install $(R) Install ALL dependencies (Prod + Dev)."
|
|
||||||
@echo " $(T)install-hooks $(R) Install git pre-commit hooks."
|
|
||||||
@echo ""
|
|
||||||
@echo "$(G)Docker:$(R)"
|
|
||||||
@echo " $(T)build $(R) Build the docker image (production)."
|
|
||||||
@echo " $(T)build-test $(R) Build the docker image (with dev deps for testing)."
|
|
||||||
@echo " $(T)down $(R) Stop and remove containers."
|
|
||||||
@echo " $(T)logs $(R) Follow logs."
|
|
||||||
@echo " $(T)prune $(R) Clean Docker system."
|
|
||||||
@echo " $(T)ps $(R) Show container status."
|
|
||||||
@echo " $(T)restart $(R) Restart all containers."
|
|
||||||
@echo " $(T)shell $(R) Open shell in container."
|
|
||||||
@echo " $(T)up $(R) Start the agent."
|
|
||||||
@echo ""
|
|
||||||
@echo "$(G)Development:$(R)"
|
|
||||||
@echo " $(T)add ... $(R) Add dependency (use --group dev or --dev if needed)."
|
|
||||||
@echo " $(T)clean $(R) Clean caches."
|
|
||||||
@echo " $(T)coverage $(R) Run tests with coverage."
|
|
||||||
@echo " $(T)format $(R) Format code (Ruff)."
|
|
||||||
@echo " $(T)lint $(R) Lint code without fixing."
|
|
||||||
@echo " $(T)test ... $(R) Run tests (local with $(RUNNER))."
|
|
||||||
@echo " $(T)update $(R) Update dependencies."
|
|
||||||
@echo ""
|
|
||||||
@echo "$(G)Versioning:$(R)"
|
|
||||||
@echo " $(T)major/minor/patch $(R) Bump version and push tag (triggers CI/CD)."
|
|
||||||
|
|
||||||
init-dotenv:
|
|
||||||
@echo "$(T)🔑 Initializing .env file...$(R)"
|
|
||||||
@if [ -f .env ]; then \
|
|
||||||
echo "$(R)⚠️ .env already exists. Skipping.$(R)"; \
|
|
||||||
exit 0; \
|
|
||||||
fi
|
|
||||||
@if [ ! -f .env.example ]; then \
|
|
||||||
echo "$(R)❌ .env.example not found$(R)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@if ! command -v openssl >/dev/null 2>&1; then \
|
|
||||||
echo "$(R)❌ openssl not found. Please install it first.$(R)"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
@echo "$(T) → Copying .env.example...$(R)"
|
|
||||||
@cp .env.example .env
|
|
||||||
@echo "$(T) → Generating secrets...$(R)"
|
|
||||||
@sed -i.bak "s|JWT_SECRET=.*|JWT_SECRET=$$(openssl rand -base64 32)|" .env
|
|
||||||
@sed -i.bak "s|JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$$(openssl rand -base64 32)|" .env
|
|
||||||
@sed -i.bak "s|CREDS_KEY=.*|CREDS_KEY=$$(openssl rand -hex 16)|" .env
|
|
||||||
@sed -i.bak "s|CREDS_IV=.*|CREDS_IV=$$(openssl rand -hex 8)|" .env
|
|
||||||
@sed -i.bak "s|MEILI_MASTER_KEY=.*|MEILI_MASTER_KEY=$$(openssl rand -base64 32)|" .env
|
|
||||||
@rm -f .env.bak
|
|
||||||
@echo "$(G)✅ .env created with generated secrets!$(R)"
|
|
||||||
@echo "$(T)⚠️ Don't forget to add your API keys:$(R)"
|
|
||||||
@echo " - OPENAI_API_KEY"
|
|
||||||
@echo " - DEEPSEEK_API_KEY"
|
|
||||||
@echo " - TMDB_API_KEY (optional)"
|
|
||||||
|
|
||||||
install: check-runner
|
|
||||||
@echo "$(T)📦 Installing FULL environment ($(RUNNER))...$(R)"
|
|
||||||
$(RUNNER_INSTALL)
|
|
||||||
@echo "✅ Environment ready (Prod + Dev)."
|
|
||||||
|
|
||||||
install-hooks: check-runner
|
|
||||||
@echo "$(T)🔧 Installing hooks...$(R)"
|
|
||||||
$(RUNNER_HOOKS)
|
|
||||||
@echo "✅ Hooks ready."
|
|
||||||
|
|
||||||
lint: check-runner
|
|
||||||
@echo "$(T)🔍 Linting code...$(R)"
|
|
||||||
$(RUNNER_RUN) ruff check .
|
|
||||||
|
|
||||||
logs: check-docker
|
|
||||||
@echo "$(T)📋 Following logs...$(R)"
|
|
||||||
$(COMPOSE_CMD) logs -f
|
|
||||||
|
|
||||||
major: _check_branch
|
|
||||||
@echo "$(T)💥 Bumping major...$(R)"
|
|
||||||
SKIP=all $(BUMP_CMD) major
|
|
||||||
@$(MAKE) -s _push_tag
|
|
||||||
|
|
||||||
minor: _check_branch
|
|
||||||
@echo "$(T)✨ Bumping minor...$(R)"
|
|
||||||
SKIP=all $(BUMP_CMD) minor
|
|
||||||
@$(MAKE) -s _push_tag
|
|
||||||
|
|
||||||
patch: _check_branch
|
|
||||||
@echo "$(T)🚀 Bumping patch...$(R)"
|
|
||||||
SKIP=all $(BUMP_CMD) patch
|
|
||||||
@$(MAKE) -s _push_tag
|
|
||||||
|
|
||||||
prune: check-docker
|
|
||||||
@echo "$(T)🗑️ Pruning Docker resources...$(R)"
|
|
||||||
docker system prune -af
|
|
||||||
@echo "✅ Docker cleaned."
|
|
||||||
|
|
||||||
ps: check-docker
|
|
||||||
@echo "$(T)📋 Container status:$(R)"
|
|
||||||
@$(COMPOSE_CMD) ps
|
|
||||||
|
|
||||||
python-version:
|
|
||||||
@echo "🔍 Reading pyproject.toml..."
|
|
||||||
@echo "✅ Python version : $(PYTHON_VERSION)"
|
|
||||||
@echo "ℹ️ Sera utilisé pour : FROM python:$(PYTHON_VERSION)-slim"
|
|
||||||
|
|
||||||
|
|
||||||
restart: check-docker
|
|
||||||
@echo "$(T)🔄 Restarting containers...$(R)"
|
|
||||||
$(COMPOSE_CMD) restart
|
|
||||||
@echo "✅ Containers restarted."
|
|
||||||
|
|
||||||
run: check-runner
|
|
||||||
$(RUNNER_RUN) $(ARGS)
|
|
||||||
|
|
||||||
shell: check-docker
|
|
||||||
@echo "$(T)🐚 Opening shell in $(SERVICE_NAME)...$(R)"
|
|
||||||
$(COMPOSE_CMD) exec $(SERVICE_NAME) /bin/sh
|
|
||||||
|
|
||||||
test: check-runner
|
|
||||||
@echo "$(T)🧪 Running tests...$(R)"
|
|
||||||
$(RUNNER_RUN) pytest $(ARGS)
|
|
||||||
|
|
||||||
up: check-docker
|
|
||||||
@echo "$(T)🚀 Starting Agent Media...$(R)"
|
|
||||||
$(COMPOSE_CMD) up -d
|
|
||||||
@echo "✅ System is up."
|
|
||||||
|
|
||||||
update: check-runner
|
|
||||||
@echo "$(T)🔄 Updating dependencies...$(R)"
|
|
||||||
$(RUNNER_UPDATE)
|
|
||||||
@echo "✅ All packages up to date."
|
|
||||||
|
|
||||||
_check_branch:
|
|
||||||
@curr=$$(git rev-parse --abbrev-ref HEAD); \
|
|
||||||
if [ "$$curr" != "main" ]; then \
|
|
||||||
echo "❌ Error: not on the main branch"; exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
_ci-dump-config:
|
_ci-dump-config:
|
||||||
@echo "image_name=$(IMAGE_NAME)"
|
@echo "image_name=$(IMAGE_NAME)"
|
||||||
@@ -248,15 +158,46 @@ _ci-dump-config:
|
|||||||
@echo "runner=$(RUNNER)"
|
@echo "runner=$(RUNNER)"
|
||||||
@echo "service_name=$(SERVICE_NAME)"
|
@echo "service_name=$(SERVICE_NAME)"
|
||||||
|
|
||||||
_ci-run-tests: build-test
|
_ci-run-tests:
|
||||||
@echo "$(T)🧪 Running tests in Docker...$(R)"
|
@echo "Running tests in Docker..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-e DEEPSEEK_API_KEY \
|
-e DEEPSEEK_API_KEY \
|
||||||
-e TMDB_API_KEY \
|
-e TMDB_API_KEY \
|
||||||
|
-e QBITTORRENT_URL \
|
||||||
$(IMAGE_NAME):test pytest
|
$(IMAGE_NAME):test pytest
|
||||||
@echo "✅ Tests passed."
|
@echo "✓ Tests passed."
|
||||||
|
|
||||||
_push_tag:
|
_check-main:
|
||||||
@echo "$(T)📦 Pushing tag...$(R)"
|
@test "$$(git rev-parse --abbrev-ref HEAD)" = "main" \
|
||||||
git push --tags
|
|| (echo "✗ ERROR: Not on main branch" && exit 1)
|
||||||
@echo "✅ Tag pushed. Check CI for build status."
|
|
||||||
|
# --- Help ---
|
||||||
|
help:
|
||||||
|
@echo "Usage: make [target]"
|
||||||
|
@echo ""
|
||||||
|
@echo "Setup:"
|
||||||
|
@echo " setup Initialize .env"
|
||||||
|
@echo " status Show project status"
|
||||||
|
@echo ""
|
||||||
|
@echo "Docker:"
|
||||||
|
@echo " up Start containers"
|
||||||
|
@echo " down Stop containers"
|
||||||
|
@echo " restart Restart containers"
|
||||||
|
@echo " logs Follow logs"
|
||||||
|
@echo " ps Container status"
|
||||||
|
@echo " shell Shell into container"
|
||||||
|
@echo " build Build image"
|
||||||
|
@echo ""
|
||||||
|
@echo "Dev:"
|
||||||
|
@echo " install Install dependencies"
|
||||||
|
@echo " update Update dependencies"
|
||||||
|
@echo " test Run tests"
|
||||||
|
@echo " coverage Run tests with coverage"
|
||||||
|
@echo " lint Lint code"
|
||||||
|
@echo " format Format code"
|
||||||
|
@echo " clean Clean artifacts"
|
||||||
|
@echo ""
|
||||||
|
@echo "Release:"
|
||||||
|
@echo " patch Bump patch version"
|
||||||
|
@echo " minor Bump minor version"
|
||||||
|
@echo " major Bump major version"
|
||||||
232
cli.py
Normal file
232
cli.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import StrEnum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
REQUIRED_VARS = ["DEEPSEEK_API_KEY", "TMDB_API_KEY", "QBITTORRENT_URL"]
|
||||||
|
|
||||||
|
# Size in bytes
|
||||||
|
KEYS_TO_GENERATE = {
|
||||||
|
"JWT_SECRET": 32,
|
||||||
|
"JWT_REFRESH_SECRET": 32,
|
||||||
|
"CREDS_KEY": 32,
|
||||||
|
"CREDS_IV": 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Style(StrEnum):
|
||||||
|
"""ANSI codes for styling output.
|
||||||
|
Usage: f"{Style.RED}Error{Style.RESET}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
|
||||||
|
|
||||||
|
# Only for terminals and if not specified otherwise
|
||||||
|
USE_COLORS = sys.stdout.isatty() and "NO_COLOR" not in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
def styled(text: str, color_code: str) -> str:
|
||||||
|
"""Apply color only if supported by the terminal."""
|
||||||
|
if USE_COLORS:
|
||||||
|
return f"{color_code}{text}{Style.RESET}"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str, color: str | None = None, prefix="") -> None:
|
||||||
|
"""Print a formatted message."""
|
||||||
|
formatted_msg = styled(msg, color) if color else msg
|
||||||
|
print(f"{prefix}{formatted_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def error_exit(msg: str) -> NoReturn:
|
||||||
|
"""Print an error message in red and exit."""
|
||||||
|
log(f"❌ {msg}", Style.RED)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def is_docker_running() -> bool:
|
||||||
|
""" "Check if Docker is available and responsive."""
|
||||||
|
if shutil.which("docker") is None:
|
||||||
|
error_exit("Docker is not installed.")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "info"],
|
||||||
|
# Redirect stdout/stderr to keep output clean on success
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
# Prevent exception being raised
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_env(content: str) -> dict[str, str]:
|
||||||
|
"""Parses existing keys and values into a dict (ignoring comments)."""
|
||||||
|
env_vars = {}
|
||||||
|
for raw_line in content.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
if line and not line.startswith("#") and "=" in line:
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
env_vars[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
|
||||||
|
def dump_env(content: str, data: dict[str, str]) -> str:
|
||||||
|
new_content: list[str] = []
|
||||||
|
processed_keys = set()
|
||||||
|
|
||||||
|
for raw_line in content.splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
# Fast line (empty, comment or not an assignation)
|
||||||
|
if len(line) == 0 or line.startswith("#") or "=" not in line:
|
||||||
|
new_content.append(raw_line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Slow line (inline comment to be kept)
|
||||||
|
key_chunk, value_chunk = raw_line.split("=", 1)
|
||||||
|
key = key_chunk.strip()
|
||||||
|
|
||||||
|
# Not in the update list
|
||||||
|
if key not in data:
|
||||||
|
new_content.append(raw_line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
processed_keys.add(key)
|
||||||
|
new_value = data[key]
|
||||||
|
|
||||||
|
if " #" not in value_chunk:
|
||||||
|
new_line = f"{key_chunk}={new_value}"
|
||||||
|
else:
|
||||||
|
_, comment = value_chunk.split(" #", 1)
|
||||||
|
new_line = f"{key_chunk}={new_value} #{comment}"
|
||||||
|
|
||||||
|
new_content.append(new_line)
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
if key not in processed_keys:
|
||||||
|
new_content.append(f"{key}={value}")
|
||||||
|
|
||||||
|
return "\n".join(new_content) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_env() -> None:
|
||||||
|
"""Manage .env lifecycle: creation, secret generation, prompts."""
|
||||||
|
env_path = Path(".env")
|
||||||
|
env_example_path = Path(".env.example")
|
||||||
|
updated: bool = False
|
||||||
|
|
||||||
|
# Read .env if exists
|
||||||
|
if env_path.exists():
|
||||||
|
content: str = env_path.read_text(encoding="utf-8")
|
||||||
|
else:
|
||||||
|
content: str = env_example_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
existing_vars: dict[str, str] = parse_env(content)
|
||||||
|
|
||||||
|
# Generate missing secrets
|
||||||
|
for key, length in KEYS_TO_GENERATE.items():
|
||||||
|
if key not in existing_vars or not existing_vars[key]:
|
||||||
|
log(f"Generating {key}...", Style.GREEN, prefix=" ")
|
||||||
|
existing_vars[key] = secrets.token_hex(length)
|
||||||
|
updated = True
|
||||||
|
log("Done", Style.GREEN, prefix=" ")
|
||||||
|
|
||||||
|
# Prompt for missing mandatory keys
|
||||||
|
color = Style.YELLOW if USE_COLORS else ""
|
||||||
|
reset = Style.RESET if USE_COLORS else ""
|
||||||
|
for key in REQUIRED_VARS:
|
||||||
|
if key not in existing_vars or not existing_vars[key]:
|
||||||
|
try:
|
||||||
|
existing_vars[key] = input(
|
||||||
|
f" {color}Enter value for {key}: {reset}"
|
||||||
|
).strip()
|
||||||
|
updated = True
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
error_exit("Aborted by user.")
|
||||||
|
|
||||||
|
# Write to disk
|
||||||
|
if updated:
|
||||||
|
# But backup original first
|
||||||
|
if env_path.exists():
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
backup_path = Path(f"{env_path}.{timestamp}.bak")
|
||||||
|
shutil.copy(env_path, backup_path)
|
||||||
|
log(f"Backup created: {backup_path}", Style.DIM)
|
||||||
|
|
||||||
|
new_content = dump_env(content, existing_vars)
|
||||||
|
env_path.write_text(new_content, encoding="utf-8")
|
||||||
|
log(".env updated successfully.", Style.GREEN)
|
||||||
|
else:
|
||||||
|
log("Configuration is up to date.", Style.GREEN)
|
||||||
|
|
||||||
|
|
||||||
|
def setup() -> None:
|
||||||
|
"""Orchestrate initialization."""
|
||||||
|
is_docker_running()
|
||||||
|
ensure_env()
|
||||||
|
#mkdir
|
||||||
|
|
||||||
|
|
||||||
|
def status() -> None:
|
||||||
|
"""Display simple dashboard."""
|
||||||
|
# Hardcoded bold style for title if colors are enabled
|
||||||
|
title_style = Style.BOLD if USE_COLORS else ""
|
||||||
|
reset_style = Style.RESET if USE_COLORS else ""
|
||||||
|
|
||||||
|
print(f"\n{title_style}ALFRED STATUS{reset_style}")
|
||||||
|
print(f"{title_style}==============={reset_style}\n")
|
||||||
|
|
||||||
|
# Docker Check
|
||||||
|
if is_docker_running():
|
||||||
|
print(f" Docker: {styled('✓ running', Style.GREEN)}")
|
||||||
|
else:
|
||||||
|
print(f" Docker: {styled('✗ stopped', Style.RED)}")
|
||||||
|
|
||||||
|
# Env Check
|
||||||
|
if Path(".env").exists():
|
||||||
|
print(f" .env: {styled('✓ present', Style.GREEN)}")
|
||||||
|
else:
|
||||||
|
print(f" .env: {styled('✗ missing', Style.RED)}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def check() -> None:
|
||||||
|
"""Silent check for prerequisites (used by 'make up')."""
|
||||||
|
setup()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python cli.py [setup|check|status]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "setup":
|
||||||
|
setup()
|
||||||
|
elif cmd == "check":
|
||||||
|
check()
|
||||||
|
elif cmd == "status":
|
||||||
|
status()
|
||||||
|
else:
|
||||||
|
error_exit(f"Unknown command: {cmd}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script to generate secure keys for LibreChat
|
|
||||||
# Run this script to generate random secure keys for your .env file
|
|
||||||
|
|
||||||
echo "==================================="
|
|
||||||
echo "LibreChat Security Keys Generator"
|
|
||||||
echo "==================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "# MongoDB Password"
|
|
||||||
echo "MONGO_PASSWORD=$(openssl rand -base64 24)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "# JWT Secrets"
|
|
||||||
echo "JWT_SECRET=$(openssl rand -base64 32)"
|
|
||||||
echo "JWT_REFRESH_SECRET=$(openssl rand -base64 32)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "# Credentials Encryption Keys"
|
|
||||||
echo "CREDS_KEY=$(openssl rand -hex 16)"
|
|
||||||
echo "CREDS_IV=$(openssl rand -hex 8)"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "==================================="
|
|
||||||
echo "Copy these values to your .env file"
|
|
||||||
echo "==================================="
|
|
||||||
Reference in New Issue
Block a user