devrig 0.30.1

Local development orchestrator
Documentation
use anyhow::Result;
use std::path::Path;

pub fn run() -> Result<()> {
    let cwd = std::env::current_dir()?;
    let config_path = cwd.join("devrig.toml");

    if config_path.exists() {
        anyhow::bail!("devrig.toml already exists in {}", cwd.display());
    }

    // Detect project type
    let project_name = cwd
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "my-project".to_string());

    let (service_name, service_command) = detect_project_type(&cwd);

    let config = format!(
        r#"[project]
name = "{project_name}"
# env_file = ".env"            # Load shared secrets from a .env file

# -- Global env vars shared by all services (supports {{{{ }}}} templates) --
# [env]
# RUST_LOG = "debug"
# NODE_ENV = "development"
# SECRET_KEY = "$MY_SECRET_KEY" # $VAR expands from .env or host environment
# DATABASE_URL = "postgres://devrig:devrig@localhost:{{{{ docker.postgres.port }}}}/{project_name}"

# -- Dashboard + OpenTelemetry --
# Built-in dashboard and OTel collector. Services automatically receive
# OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME. Ports auto-resolve
# if already in use, so multiple devrig instances can coexist.
[dashboard]
# port = 4000                    # default; auto-resolves if in use
# OTel defaults: grpc_port=4317, http_port=4318, retention="1h" — customize with [dashboard.otel]

# -- OIDC provider (built-in, replaces Keycloak/dex for local dev) --
# Spins up an in-process OAuth2 / OIDC provider with pre-seeded users + clients.
# Services receive `oidc.issuer` and `oidc.port` template vars.
#
# [oidc]
# port = "auto"
# realm = "{project_name}"
# audience = "{project_name}-api"   # written as `aud` on access tokens
#
# [[oidc.users]]
# email = "admin@example.com"
# password = "admin"
# name = "Admin"
# role = "admin"
#
# [oidc.clients.web]
# public = true                                                      # PKCE, no client_secret
# redirect_uris = ["http://localhost:{{{{ services.{service_name}.port }}}}/auth/callback"]

# -- Links --
# Named URLs for services devrig doesn't manage (shown in dashboard).
# [links]
# headlamp = "http://localhost:8080"
# grafana = "http://localhost:3000"

# -- Services --
[services.{service_name}]
command = "{service_command}"
# port = 3000
# path = "./"
# depends_on = ["postgres"]
#
# env_file = ".env.{service_name}"  # Per-service .env file
#
# [services.{service_name}.env]
# DATABASE_URL = "postgres://user:${{DB_PASS}}@localhost:{{{{ docker.postgres.port }}}}/mydb"
# KUBECONFIG = "{{{{ cluster.kubeconfig }}}}"  # when service needs k3d access
#
# [services.{service_name}.restart]
# policy = "on-failure"
# max_restarts = 10

# [services.worker]
# command = "cargo run --bin worker"
# depends_on = ["{service_name}"]

# -- Docker containers --
# devrig manages Docker containers with health checks, init scripts, and volumes.
#
# [docker.postgres]
# image = "postgres:16-alpine"
# port = 5432                                     # host port
# # container_port = 5432                         # internal port (if different from host port)
# volumes = ["pgdata:/var/lib/postgresql/data"]   # named volume
# # volumes = ["./data:/var/lib/postgresql/data"] # or bind mount (host dir)
# ready_check = {{ type = "pg_isready" }}
# init = ["CREATE DATABASE {project_name};"]
#
# [docker.postgres.env]
# POSTGRES_USER = "devrig"
# POSTGRES_PASSWORD = "devrig"
#
# [docker.redis]
# image = "redis:7-alpine"
# port = 6379
# command = ["redis-server", "--appendonly", "yes"]  # override CMD
# ready_check = {{ type = "cmd", command = "redis-cli ping", expect = "PONG" }}
#
# -- Custom entrypoint --
# [docker.worker]
# image = "python:3.12-slim"
# entrypoint = ["python", "-u"]    # override ENTRYPOINT
# command = ["worker.py"]           # override CMD (args to entrypoint)
#
# -- Private registry images --
# [docker.my-app]
# image = "ghcr.io/org/app:latest"
# registry_auth = {{ username = "$REGISTRY_USER", password = "$REGISTRY_TOKEN" }}

# -- Docker Compose integration --
# Delegate to an existing docker-compose.yml.
# Services are auto-discovered from the file; list specific ones to limit.
#
# [compose]
# file = "docker-compose.yml"
# services = ["redis", "postgres"]  # Optional — empty auto-discovers all

# -- Kubernetes cluster (k3d) --
# Create a local cluster with auto-build and deploy.
#
# [cluster]
# agents = 1
# ports = ["8080:80"]
# volumes = ["../:/workspace@server:*"]  # mount host dirs into cluster nodes
# k3s_args = ["--disable=traefik"]       # extra flags passed to k3s
#
# [cluster.image.job-runner]
# context = "./tools/job-runner"
# # dockerfile = "Dockerfile"   # optional, defaults to Dockerfile
# watch = true
# # [cluster.image.job-runner.build_secrets]
# # cargo_token = "~/.cargo/credentials.toml"  # BuildKit --secret
# # [cluster.image.job-runner.build_args]
# # SERVER_IMAGE = "{{{{ cluster.image.server.tag }}}}"  # reference another image
#
# [cluster.deploy.api]
# context = "./services/api"
# manifests = ["k8s/deployment.yaml", "k8s/service.yaml"]
# watch = true
# depends_on = ["job-runner"]   # ensures image is built before deploy
#
# [cluster.addons.cert-manager]
# type = "helm"
# chart = "cert-manager/cert-manager"
# repo = "https://charts.jetstack.io"
# namespace = "cert-manager"
#
# -- Local chart with dependencies and image tag template --
# [cluster.addons.myapp]
# type = "helm"
# chart = "./charts/myapp"
# namespace = "myapp"
# depends_on = ["cert-manager"]  # installs after cert-manager
# wait = false                    # don't block on readiness (default: true)
# timeout = "10m"                 # helm timeout (default: "5m")
# skip_crds = true                # skip CRD installation (default: false)
# values_files = ["charts/myapp/values-dev.yaml"]
# [cluster.addons.myapp.values]
# "image.repository" = "{{{{ cluster.registry }}}}/myapp"
# "image.tag" = "{{{{ cluster.image.myapp.tag }}}}"
#
# -- OCI chart — no repo field needed --
# [cluster.addons.my-chart]
# type = "helm"
# chart = "oci://ghcr.io/org/charts/my-chart"
# namespace = "my-chart"
# version = "1.2.0"
#
# -- Private registry auth for cluster image pulls --
# [[cluster.registries]]
# url = "ghcr.io"
# username = "$REGISTRY_USER"
# password = "$REGISTRY_TOKEN"
"#
    );

    std::fs::write(&config_path, &config)?;
    println!("Created devrig.toml in {}", cwd.display());
    println!();
    println!("  Project: {}", project_name);
    println!("  Service: {} -> {}", service_name, service_command);
    println!();
    println!("Edit the file, then run `devrig start` to begin.");
    Ok(())
}

fn detect_project_type(dir: &Path) -> (&'static str, &'static str) {
    if dir.join("Cargo.toml").exists() {
        ("app", "cargo watch -x run")
    } else if dir.join("package.json").exists() {
        ("app", "npm run dev")
    } else if dir.join("go.mod").exists() {
        ("app", "go run .")
    } else if dir.join("requirements.txt").exists() || dir.join("pyproject.toml").exists() {
        ("app", "python main.py")
    } else {
        ("app", "echo 'Replace this with your command'")
    }
}