From 2cfe7a035bdeac871e34466ef722d103fee1e515 Mon Sep 17 00:00:00 2001 From: Francwa Date: Sat, 27 Dec 2025 19:51:40 +0100 Subject: [PATCH] infra: moved makefile logic to python and added .env setup --- Makefile | 401 ++++++++++++++++++++--------------------------- cli.py | 232 +++++++++++++++++++++++++++ generate-keys.sh | 27 ---- 3 files changed, 403 insertions(+), 257 deletions(-) create mode 100644 cli.py delete mode 100755 generate-keys.sh diff --git a/Makefile b/Makefile index 250bbbd..6734100 100644 --- a/Makefile +++ b/Makefile @@ -1,245 +1,155 @@ -.POSIX: -.SUFFIXES: .DEFAULT_GOAL := help -# --- SETTINGS --- -IMAGE_NAME = alfred_media_organizer -# renovate: datasource=docker depName=python -PYTHON_VERSION = $(shell grep "python" pyproject.toml | head -n 1 | sed -E 's/.*[=<>^~"]+ *([0-9]+\.[0-9]+(\.[0-9]+)?).*/\1/') -PYTHON_VERSION_SHORT = $(shell echo $(PYTHON_VERSION) | cut -d. -f1,2) -# Change to 'uv' when ready. -RUNNER ?= poetry -SERVICE_NAME = alfred +# --- Config --- +export IMAGE_NAME := alfred_media_organizer +export LIBRECHAT_VERSION := v0.8.1 +export PYTHON_VERSION := 3.14.2 +export PYTHON_VERSION_SHORT := 3.14 +export RAG_VERSION := v0.7.0 +export RUNNER := poetry +export SERVICE_NAME := alfred -export IMAGE_NAME -export PYTHON_VERSION -export PYTHON_VERSION_SHORT -export RUNNER +# --- Commands --- +CLI := python3 cli.py +DOCKER_COMPOSE := docker compose +DOCKER_BUILD := docker build \ + --build-arg PYTHON_VERSION=$(PYTHON_VERSION) \ + --build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \ + --build-arg RUNNER=$(RUNNER) -# --- ADAPTERS --- -# UV uses "sync", Poetry uses "install". Both install DEV deps by default. -INSTALL_CMD = $(if $(filter uv,$(RUNNER)),sync,install) +# --- Phony --- +.PHONY: setup status check +.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 --- -ARGS = $(filter-out $@,$(MAKECMDGOALS)) -BUMP_CMD = $(RUNNER) run bump-my-version bump -COMPOSE_CMD = docker-compose -DOCKER_CMD = docker build \ - --build-arg PYTHON_VERSION=$(PYTHON_VERSION) \ - --build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \ - --build-arg RUNNER=$(RUNNER) \ - -t $(IMAGE_NAME):latest . +# --- Setup --- +setup: + @echo "Initializing environment..." + @$(CLI) setup \ + && echo "✓ Environment ready" \ + || (echo "✗ Setup failed" && exit 1) -RUNNER_ADD = $(RUNNER) add -RUNNER_HOOKS = $(RUNNER) run pre-commit install -c ../.pre-commit-config.yaml -RUNNER_INSTALL = $(RUNNER) $(INSTALL_CMD) -RUNNER_RUN = $(RUNNER) run -RUNNER_UPDATE = $(RUNNER) update +status: + @$(CLI) status -# --- STYLES --- -B = \033[1m -G = \033[32m -T = \033[36m -R = \033[0m +check: + @$(CLI) check -# --- TARGETS --- -.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 +# --- Docker --- +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 - @echo "$(T)➕ Adding dependency ($(RUNNER)): $(ARGS)$(R)" - $(RUNNER_ADD) $(ARGS) +restart: + @echo "Restarting containers..." + @$(DOCKER_COMPOSE) restart \ + && echo "✓ Containers restarted" \ + || (echo "✗ Failed to restart containers" && exit 1) -build: check-docker - @echo "$(T)🐳 Building Docker image...$(R)" - $(DOCKER_CMD) - @echo "✅ Image $(IMAGE_NAME):latest ready." +logs: + @echo "Following logs (Ctrl+C to exit)..." + @$(DOCKER_COMPOSE) logs -f -build-test: check-docker - @echo "$(T)🐳 Building test image (with dev deps)...$(R)" - docker build \ - --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." +ps: + @echo "Container status:" + @$(DOCKER_COMPOSE) ps -check-docker: - @command -v docker >/dev/null 2>&1 || { echo "$(R)❌ Docker not installed$(R)"; exit 1; } - @docker info >/dev/null 2>&1 || { echo "$(R)❌ Docker daemon not running$(R)"; exit 1; } +shell: + @echo "Opening shell in $(SERVICE_NAME)..." + @$(DOCKER_COMPOSE) exec $(SERVICE_NAME) /bin/bash -check-runner: - @command -v $(RUNNER) >/dev/null 2>&1 || { echo "$(R)❌ $(RUNNER) not installed$(R)"; exit 1; } +# --- Build --- +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: - @echo "$(T)🧹 Cleaning caches...$(R)" - rm -rf .ruff_cache __pycache__ .pytest_cache - 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 - find . -type f -name "*.pyc" -delete 2>/dev/null || true - @echo "✅ Caches cleaned." + @echo "Cleaning build artifacts..." + @rm -rf .ruff_cache __pycache__ .pytest_cache htmlcov .coverage + @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + @echo "✓ Cleanup complete" -coverage: check-runner - @echo "$(T)📊 Running tests with coverage...$(R)" - $(RUNNER_RUN) pytest --cov=. --cov-report=html --cov-report=term $(ARGS) - @echo "✅ Report generated in htmlcov/" +prune: + @echo "Pruning Docker system..." + @docker system prune -af \ + && echo "✓ Docker pruned" \ + || (echo "✗ Prune failed" && exit 1) -down: check-docker - @echo "$(T)🛑 Stopping containers...$(R)" - $(COMPOSE_CMD) down - @echo "✅ System stopped." +# --- Versioning --- +major minor patch: _check-main + @echo "Bumping $@ version..." + @$(RUNNER) run bump-my-version bump $@ \ + && echo "✓ Version bumped" \ + || (echo "✗ Version bump failed" && exit 1) -format: check-runner - @echo "$(T)✨ Formatting with Ruff...$(R)" - $(RUNNER_RUN) ruff format . - $(RUNNER_RUN) ruff check --fix . - @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 + @echo "Pushing tags..." + @git push --tags \ + && echo "✓ Tags pushed" \ + || (echo "✗ Push failed" && exit 1) _ci-dump-config: @echo "image_name=$(IMAGE_NAME)" @@ -248,15 +158,46 @@ _ci-dump-config: @echo "runner=$(RUNNER)" @echo "service_name=$(SERVICE_NAME)" -_ci-run-tests: build-test - @echo "$(T)🧪 Running tests in Docker...$(R)" +_ci-run-tests: + @echo "Running tests in Docker..." docker run --rm \ -e DEEPSEEK_API_KEY \ -e TMDB_API_KEY \ + -e QBITTORRENT_URL \ $(IMAGE_NAME):test pytest - @echo "✅ Tests passed." + @echo "✓ Tests passed." -_push_tag: - @echo "$(T)📦 Pushing tag...$(R)" - git push --tags - @echo "✅ Tag pushed. Check CI for build status." +_check-main: + @test "$$(git rev-parse --abbrev-ref HEAD)" = "main" \ + || (echo "✗ ERROR: Not on main branch" && exit 1) + +# --- 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" \ No newline at end of file diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..a762bdf --- /dev/null +++ b/cli.py @@ -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() diff --git a/generate-keys.sh b/generate-keys.sh deleted file mode 100755 index 3923201..0000000 --- a/generate-keys.sh +++ /dev/null @@ -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 "==================================="