Add mage dev-env task for isolated worktree dev environments (#70602)
* Add worktree dev environment tooling Add `mage dev-env` task for isolated worktree dev environments with Docker containers and deterministic per-worktree ports. Includes workmux layout with nREPL dev server, frontend build, and nvim panes. Adds a custom Claude Code Docker image with Clojure tooling via mise, and a bin/claude-dangerous helper script. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move dev-env mage task to personal -edpaget-dev-env task Move the worktree dev environment tooling from the shared mage task namespace (mage.dev-env) to a personal task namespace (mage.edpaget.dev-env), remove the sample .workmux.example.yaml, and update .workmux.yaml hooks to use the new command name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,6 +25,7 @@
|
||||
/.env*
|
||||
!/.env.example
|
||||
/.envrc
|
||||
/.workmux.yaml
|
||||
/.lein-env
|
||||
/.lein-failures
|
||||
/.lein-plugins
|
||||
|
||||
18
bb.edn
18
bb.edn
@@ -533,4 +533,20 @@
|
||||
:requires [[mage.escherize.fix-whitespace :as fix-whitespace]]
|
||||
:examples [["mage -escherize-fix-whitespace '**/*.clj{,c,s}'" "Removes all lines like ' ' from all clj files"]]
|
||||
:arg-schema [:sequential [:string {:name "glob-pattern" :description "Glob pattern to match files"}]]
|
||||
:task (task! (fix-whitespace/run-clean parsed))}}}
|
||||
:task (task! (fix-whitespace/run-clean parsed))}
|
||||
|
||||
-edpaget-dev-env
|
||||
{:doc "Stand up (or tear down) an isolated dev environment for this worktree"
|
||||
:examples [["./bin/mage -edpaget-dev-env --app-db postgres" "Postgres app-db with auto ports"]
|
||||
["./bin/mage -edpaget-dev-env --app-db postgres --with mysql --with mongo" "Full stack"]
|
||||
["./bin/mage -edpaget-dev-env" "Minimal: just port config (H2 app-db)"]
|
||||
["./bin/mage -edpaget-dev-env --down" "Tear down containers for this worktree"]
|
||||
["./bin/mage -edpaget-dev-env --status" "Show running containers and ports"]]
|
||||
:requires [[mage.edpaget.dev-env :as dev-env]]
|
||||
:options [[nil "--app-db DB" "App database: h2 (default), postgres, mysql" :default "h2"]
|
||||
[nil "--with SERVICE" "Warehouse service (repeatable): postgres, mysql, mariadb, mongo, clickhouse, ldap, maildev"
|
||||
:multi true :default [] :update-fn conj]
|
||||
[nil "--down" "Tear down all containers for this worktree"]
|
||||
[nil "--status" "Show current env status"]
|
||||
[nil "--slot SLOT" "Override port slot (0-99)" :parse-fn Integer/parseInt]]
|
||||
:task (task! (dev-env/dev-env! parsed))}}}
|
||||
|
||||
96
bin/claude-dangerous
Executable file
96
bin/claude-dangerous
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs Claude Code in a custom Docker container with --dangerously-skip-permissions.
|
||||
# The image includes Clojure, Java, Babashka, and all tools from mise.toml.
|
||||
# DB connection info is forwarded from mise.local.toml.
|
||||
#
|
||||
# Usage: workmux add --agent ./bin/claude-dangerous feat/my-branch
|
||||
#
|
||||
# Pass --build as the first arg to force a rebuild of the image.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
IMAGE_NAME="metabase-claude-code"
|
||||
DOCKERFILE="$PROJECT_ROOT/dev/docker/claude-code/Dockerfile"
|
||||
|
||||
# Build image if --build passed or image doesn't exist
|
||||
if [[ "${1:-}" == "--build" ]]; then
|
||||
shift
|
||||
echo "Building $IMAGE_NAME image..."
|
||||
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$PROJECT_ROOT"
|
||||
elif ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
|
||||
echo "Image $IMAGE_NAME not found, building..."
|
||||
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" "$PROJECT_ROOT"
|
||||
fi
|
||||
|
||||
# On macOS, extract Claude OAuth credentials from the system keychain
|
||||
# and prepare a minimal .claude.json for the container.
|
||||
AUTH_TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$AUTH_TMPDIR"' EXIT
|
||||
|
||||
AUTH_MOUNT_ARGS=()
|
||||
if [[ "$OSTYPE" == darwin* ]]; then
|
||||
KEYCHAIN_CREDS=$(security find-generic-password -s "Claude Code-credentials" -a "$USER" -w 2>/dev/null || true)
|
||||
if [[ -n "$KEYCHAIN_CREDS" ]]; then
|
||||
mkdir -p "$AUTH_TMPDIR/.claude"
|
||||
echo "$KEYCHAIN_CREDS" > "$AUTH_TMPDIR/.claude/.credentials.json"
|
||||
AUTH_MOUNT_ARGS+=(-v "$AUTH_TMPDIR/.claude/.credentials.json:/home/claude/.claude/.credentials.json")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build a minimal .claude.json with onboarding complete and project trust.
|
||||
# Mounting the host's full ~/.claude.json won't work (host-specific paths).
|
||||
python3 -c "
|
||||
import json, os
|
||||
host_config = '$HOME/.claude.json'
|
||||
config = {}
|
||||
if os.path.exists(host_config):
|
||||
with open(host_config) as f:
|
||||
full = json.load(f)
|
||||
# Copy only the auth-related keys
|
||||
for key in ('oauthAccount', 'customApiKeyResponses'):
|
||||
if key in full:
|
||||
config[key] = full[key]
|
||||
config['hasCompletedOnboarding'] = True
|
||||
config['bypassPermissionsModeAccepted'] = True
|
||||
config['dangerouslySkipPermissions'] = True
|
||||
config['autoAcceptPermissions'] = True
|
||||
config['numStartups'] = 1
|
||||
config['projects'] = {
|
||||
'/app': {
|
||||
'allowedTools': [],
|
||||
'hasTrustDialogAccepted': True,
|
||||
'skipConfirmationDialogs': True,
|
||||
}
|
||||
}
|
||||
with open('$AUTH_TMPDIR/claude.json', 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
"
|
||||
AUTH_MOUNT_ARGS+=(-v "$AUTH_TMPDIR/claude.json:/home/claude/.claude.json")
|
||||
|
||||
# Parse mise.local.toml for env vars (DB connection, ports, etc.)
|
||||
ENV_ARGS=()
|
||||
if [[ -f "$PROJECT_ROOT/mise.local.toml" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
key="$(echo "$key" | xargs)"
|
||||
value="$(echo "$value" | sed 's/^[[:space:]]*"//;s/"[[:space:]]*$//')"
|
||||
if [[ -n "$key" && -n "$value" ]]; then
|
||||
# Rewrite localhost to host.docker.internal so container can reach host DBs
|
||||
if [[ "$key" == "MB_DB_CONNECTION_URI" ]]; then
|
||||
value="${value//localhost/host.docker.internal}"
|
||||
fi
|
||||
ENV_ARGS+=(-e "$key=$value")
|
||||
fi
|
||||
done < <(grep -E '^[A-Z_]+ *= *"' "$PROJECT_ROOT/mise.local.toml")
|
||||
fi
|
||||
|
||||
exec docker run -it --rm \
|
||||
-v "$PROJECT_ROOT:/app" \
|
||||
-w /app \
|
||||
-v /dev/null:/app/mise.local.toml:ro \
|
||||
"${AUTH_MOUNT_ARGS[@]}" \
|
||||
-e ANTHROPIC_API_KEY \
|
||||
"${ENV_ARGS[@]}" \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
"$IMAGE_NAME" \
|
||||
--dangerously-skip-permissions
|
||||
38
dev/docker/claude-code/Dockerfile
Normal file
38
dev/docker/claude-code/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM ubuntu:noble
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl git rlwrap bash ca-certificates gnupg ripgrep sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user (--dangerously-skip-permissions refuses to run as root)
|
||||
RUN useradd -m -s /bin/bash claude
|
||||
RUN echo 'claude ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
USER claude
|
||||
|
||||
# Install mise
|
||||
RUN curl https://mise.run | sh
|
||||
ENV PATH="/home/claude/.local/bin:$PATH"
|
||||
|
||||
# Copy project mise.toml to install the same tool versions
|
||||
COPY --chown=claude:claude mise.toml /home/claude/.config/mise/config.toml
|
||||
|
||||
# Trust the mise config files so there's no interactive prompt
|
||||
RUN mise trust /home/claude/.config/mise/config.toml
|
||||
|
||||
# Install all tools defined in mise.toml
|
||||
RUN mise install
|
||||
|
||||
# Put mise shims on PATH
|
||||
RUN echo 'eval "$(mise activate bash)"' >> /home/claude/.bashrc
|
||||
ENV PATH="/home/claude/.local/share/mise/shims:$PATH"
|
||||
|
||||
# Auto-accept mise trust prompts for project configs mounted at runtime
|
||||
ENV MISE_YES=1
|
||||
|
||||
# Create .claude directory so Claude Code can write session data
|
||||
RUN mkdir -p /home/claude/.claude
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["claude"]
|
||||
@@ -65,7 +65,9 @@
|
||||
(def cli-spec [["-h" "--help" "Show this help text"]
|
||||
["-H" "--hot" "Enable hot reloading"] ;
|
||||
["-p" "--port PORT" "Port to run the nREPL server on"
|
||||
:default 50605
|
||||
:default (if-let [p (env/env :nrepl-port)]
|
||||
(Integer/parseInt p)
|
||||
50605)
|
||||
:parse-fn #(Integer/parseInt %)]])
|
||||
|
||||
(defn -main
|
||||
|
||||
328
mage/src/mage/edpaget/dev_env.clj
Normal file
328
mage/src/mage/edpaget/dev_env.clj
Normal file
@@ -0,0 +1,328 @@
|
||||
(ns mage.edpaget.dev-env
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[mage.color :as c]
|
||||
[mage.shell :as shell]
|
||||
[mage.util :as u]
|
||||
[table.core :as t]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Port bases
|
||||
|
||||
(def ^:private port-bases
|
||||
{:jetty 3000
|
||||
:frontend-dev 8080
|
||||
:nrepl 50605
|
||||
:socket-repl 50505
|
||||
:postgres-app 15432
|
||||
:postgres-wh 25432
|
||||
:mysql 13309
|
||||
:mariadb 13306
|
||||
:mongo 37017
|
||||
:clickhouse-http 18123
|
||||
:clickhouse-nat 19000
|
||||
:ldap 11389
|
||||
:maildev-smtp 11025
|
||||
:maildev-ui 11080})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Service specs
|
||||
|
||||
(def ^:private service-specs
|
||||
"Map of service-key -> spec. :internal-ports is a vector of [host-key internal-port] pairs."
|
||||
{:postgres {:image "postgres:17"
|
||||
:internal-ports [[:postgres-app 5432]]
|
||||
:wh-ports [[:postgres-wh 5432]]
|
||||
:env {"POSTGRES_USER" "metabase"
|
||||
"POSTGRES_DB" "metabase"
|
||||
"POSTGRES_PASSWORD" "password"
|
||||
"PGDATA" "/var/lib/postgresql/data"}
|
||||
:can-be-app-db? true}
|
||||
:mysql {:image "mysql:8.4"
|
||||
:internal-ports [[:mysql 3306]]
|
||||
:env {"MYSQL_DATABASE" "metabase_test"
|
||||
"MYSQL_ALLOW_EMPTY_PASSWORD" "yes"}
|
||||
:can-be-app-db? true}
|
||||
:mariadb {:image "mariadb:11"
|
||||
:internal-ports [[:mariadb 3306]]
|
||||
:env {"MYSQL_DATABASE" "metabase_test"
|
||||
"MYSQL_ALLOW_EMPTY_PASSWORD" "yes"}
|
||||
:can-be-app-db? true}
|
||||
:mongo {:image "mongo:8"
|
||||
:internal-ports [[:mongo 27017]]
|
||||
:env {"MONGO_INITDB_ROOT_USERNAME" "metabase"
|
||||
"MONGO_INITDB_ROOT_PASSWORD" "metasample123"}}
|
||||
:clickhouse {:image "clickhouse/clickhouse-server:latest"
|
||||
:internal-ports [[:clickhouse-http 8123] [:clickhouse-nat 9000]]
|
||||
:env {"CLICKHOUSE_USER" "metabase"
|
||||
"CLICKHOUSE_PASSWORD" "metasample123"}}
|
||||
:ldap {:image "bitnami/openldap:latest"
|
||||
:internal-ports [[:ldap 1389]]
|
||||
:env {"LDAP_ADMIN_USERNAME" "admin"
|
||||
"LDAP_ADMIN_PASSWORD" "adminpassword"}}
|
||||
:maildev {:image "maildev/maildev"
|
||||
:internal-ports [[:maildev-smtp 1025] [:maildev-ui 1080]]
|
||||
:env {}}})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Helpers
|
||||
|
||||
(defn- worktree-name
|
||||
"Last path component of the project root directory."
|
||||
[]
|
||||
(let [path u/project-root-directory]
|
||||
(last (str/split path #"/"))))
|
||||
|
||||
(defn- compute-slot
|
||||
"Deterministic slot 0-99 from worktree name, or override."
|
||||
[slot-override]
|
||||
(if slot-override
|
||||
slot-override
|
||||
(mod (Math/abs (.hashCode ^String (worktree-name))) 100)))
|
||||
|
||||
(defn- container-prefix
|
||||
"Prefix for all docker containers for this worktree."
|
||||
[]
|
||||
(str "mb-" (worktree-name) "-"))
|
||||
|
||||
(defn- port-for
|
||||
"Compute the port for a given service key and slot."
|
||||
[port-key slot]
|
||||
(+ (get port-bases port-key) slot))
|
||||
|
||||
(defn- kill-container!
|
||||
"Kill and remove a docker container (quiet, no error on missing)."
|
||||
[container-name]
|
||||
(shell/sh* {:quiet? true} "docker" "kill" container-name)
|
||||
(shell/sh* {:quiet? true} "docker" "rm" container-name))
|
||||
|
||||
(defn- check-docker!
|
||||
"Verify docker is available and the daemon is running."
|
||||
[]
|
||||
(when-not (u/can-run? "docker")
|
||||
(println (c/red "Docker is not installed. Please install Docker to use dev-env."))
|
||||
(u/exit 1))
|
||||
(let [{:keys [exit]} (shell/sh* {:quiet? true} "docker" "info")]
|
||||
(when-not (zero? exit)
|
||||
(println (c/red "Docker daemon is not running. Please start Docker."))
|
||||
(u/exit 1))))
|
||||
|
||||
(defn- build-docker-cmd
|
||||
"Build a docker run command vector for a service."
|
||||
[container-name image port-mappings env-map]
|
||||
(into ["docker" "run" "-d"]
|
||||
(concat
|
||||
(mapcat (fn [[host-port internal-port]]
|
||||
["-p" (str host-port ":" internal-port)])
|
||||
port-mappings)
|
||||
(mapcat (fn [[k v]]
|
||||
["-e" (str k "=" v)])
|
||||
env-map)
|
||||
["--name" container-name
|
||||
image])))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Core
|
||||
|
||||
(defn- start-service!
|
||||
"Kill existing container and start a new one."
|
||||
[service-key suffix slot]
|
||||
(let [spec (get service-specs service-key)
|
||||
container-name (str (container-prefix) (name service-key) (when suffix (str "-" suffix)))
|
||||
port-key-src (if (= suffix "app")
|
||||
(:internal-ports spec)
|
||||
(or (:wh-ports spec) (:internal-ports spec)))
|
||||
port-mappings (mapv (fn [[pk ip]] [(port-for pk slot) ip]) port-key-src)
|
||||
cmd (build-docker-cmd container-name (:image spec) port-mappings (:env spec))]
|
||||
(println (c/yellow "Starting " container-name "..."))
|
||||
(kill-container! container-name)
|
||||
(u/debug "Running: " (str/join " " cmd))
|
||||
(apply shell/sh cmd)
|
||||
(println (c/green " Started ") container-name)))
|
||||
|
||||
(defn- validate-services!
|
||||
"Validate the app-db and --with values. Returns normalized config map."
|
||||
[{:keys [app-db with]}]
|
||||
(let [app-db-kw (keyword app-db)
|
||||
with-kws (mapv keyword with)
|
||||
valid-app-dbs #{:h2 :postgres :mysql :mariadb}
|
||||
valid-withs #{:postgres :mysql :mariadb :mongo :clickhouse :ldap :maildev}]
|
||||
(when-not (valid-app-dbs app-db-kw)
|
||||
(println (c/red "Invalid --app-db: " app-db ". Must be one of: " (str/join ", " (map name valid-app-dbs))))
|
||||
(u/exit 1))
|
||||
(doseq [w with-kws]
|
||||
(when-not (valid-withs w)
|
||||
(println (c/red "Invalid --with: " (name w) ". Must be one of: " (str/join ", " (map name valid-withs))))
|
||||
(u/exit 1)))
|
||||
;; Warn if --with overlaps with --app-db
|
||||
(when (and (not= app-db-kw :h2) (some #{app-db-kw} with-kws))
|
||||
(println (c/yellow "Note: --with " app-db " is redundant when --app-db is " app-db ". Using app-db container only.")))
|
||||
{:app-db app-db-kw
|
||||
:with (if (= app-db-kw :h2)
|
||||
with-kws
|
||||
(vec (remove #{app-db-kw} with-kws)))}))
|
||||
|
||||
(defn- generate-mise-local!
|
||||
"Generate mise.local.toml at the project root."
|
||||
[slot {:keys [app-db]}]
|
||||
(let [wt-name (worktree-name)
|
||||
lines (cond-> [(str "# Auto-generated by ./bin/mage -edpaget-dev-env")
|
||||
(str "# Worktree: " wt-name " (slot " slot ")")
|
||||
(str "# Re-run `./bin/mage -edpaget-dev-env` to regenerate.")
|
||||
""
|
||||
"[env]"
|
||||
(str "MB_JETTY_PORT = \"" (port-for :jetty slot) "\"")
|
||||
(str "MB_FRONTEND_DEV_PORT = \"" (port-for :frontend-dev slot) "\"")
|
||||
(str "NREPL_PORT = \"" (port-for :nrepl slot) "\"")
|
||||
(str "SOCKET_REPL_PORT = \"" (port-for :socket-repl slot) "\"")]
|
||||
(= app-db :postgres)
|
||||
(conj (str "MB_DB_TYPE = \"postgres\"")
|
||||
(str "MB_DB_CONNECTION_URI = \"jdbc:postgresql://localhost:"
|
||||
(port-for :postgres-app slot)
|
||||
"/metabase?user=metabase&password=password\""))
|
||||
|
||||
(= app-db :mysql)
|
||||
(conj (str "MB_DB_TYPE = \"mysql\"")
|
||||
(str "MB_DB_CONNECTION_URI = \"jdbc:mysql://localhost:"
|
||||
(port-for :mysql slot)
|
||||
"/metabase?user=root&password=\""))
|
||||
|
||||
(= app-db :mariadb)
|
||||
(conj (str "MB_DB_TYPE = \"mysql\"")
|
||||
(str "MB_DB_CONNECTION_URI = \"jdbc:mysql://localhost:"
|
||||
(port-for :mariadb slot)
|
||||
"/metabase?user=root&password=\"")))
|
||||
content (str (str/join "\n" lines) "\n")
|
||||
path (str u/project-root-directory "/mise.local.toml")]
|
||||
(spit path content)
|
||||
(println (c/green "Wrote " path))))
|
||||
|
||||
(defn- print-summary!
|
||||
"Print a summary table of ports, containers, and connection info."
|
||||
[slot {:keys [app-db with]}]
|
||||
(let [wt-name (worktree-name)
|
||||
rows (cond-> [{:service "Jetty backend" :port (port-for :jetty slot) :env-var "MB_JETTY_PORT"}
|
||||
{:service "Frontend dev" :port (port-for :frontend-dev slot) :env-var "MB_FRONTEND_DEV_PORT"}
|
||||
{:service "nREPL" :port (port-for :nrepl slot) :env-var "NREPL_PORT"}
|
||||
{:service "Socket REPL" :port (port-for :socket-repl slot) :env-var "SOCKET_REPL_PORT"}]
|
||||
|
||||
(= app-db :postgres)
|
||||
(conj {:service "Postgres (app-db)" :port (port-for :postgres-app slot) :env-var "MB_DB_CONNECTION_URI"})
|
||||
|
||||
(= app-db :mysql)
|
||||
(conj {:service "MySQL (app-db)" :port (port-for :mysql slot) :env-var "MB_DB_CONNECTION_URI"})
|
||||
|
||||
(= app-db :mariadb)
|
||||
(conj {:service "MariaDB (app-db)" :port (port-for :mariadb slot) :env-var "MB_DB_CONNECTION_URI"})
|
||||
|
||||
(some #{:postgres} with)
|
||||
(conj {:service "Postgres (warehouse)" :port (port-for :postgres-wh slot) :env-var ""})
|
||||
|
||||
(some #{:mysql} with)
|
||||
(conj {:service "MySQL (warehouse)" :port (port-for :mysql slot) :env-var ""})
|
||||
|
||||
(some #{:mariadb} with)
|
||||
(conj {:service "MariaDB (warehouse)" :port (port-for :mariadb slot) :env-var ""})
|
||||
|
||||
(some #{:mongo} with)
|
||||
(conj {:service "MongoDB" :port (port-for :mongo slot) :env-var ""})
|
||||
|
||||
(some #{:clickhouse} with)
|
||||
(conj {:service "ClickHouse HTTP" :port (port-for :clickhouse-http slot) :env-var ""}
|
||||
{:service "ClickHouse native" :port (port-for :clickhouse-nat slot) :env-var ""})
|
||||
|
||||
(some #{:ldap} with)
|
||||
(conj {:service "OpenLDAP" :port (port-for :ldap slot) :env-var ""})
|
||||
|
||||
(some #{:maildev} with)
|
||||
(conj {:service "Maildev SMTP" :port (port-for :maildev-smtp slot) :env-var ""}
|
||||
{:service "Maildev UI" :port (port-for :maildev-ui slot) :env-var ""}))]
|
||||
(println)
|
||||
(println (c/bold (c/green "Dev environment for ") (c/cyan wt-name) (c/green " (slot " slot ")")))
|
||||
(println)
|
||||
(t/table rows :style :unicode)
|
||||
(println)
|
||||
(println (c/cyan "Open a new shell in this worktree for mise to pick up the env vars."))
|
||||
(println (c/cyan "Tear down with: ") (c/yellow "./bin/mage -edpaget-dev-env --down"))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Entry points
|
||||
|
||||
(defn- stand-up!
|
||||
"Orchestrate: validate -> check docker -> start containers -> write toml -> print summary."
|
||||
[opts]
|
||||
(let [slot (compute-slot (:slot opts))
|
||||
config (validate-services! opts)]
|
||||
(check-docker!)
|
||||
;; Start app-db container if not H2
|
||||
(when (not= (:app-db config) :h2)
|
||||
(start-service! (:app-db config) "app" slot))
|
||||
;; Start warehouse containers
|
||||
(doseq [svc (:with config)]
|
||||
(start-service! svc "wh" slot))
|
||||
;; Write mise.local.toml
|
||||
(generate-mise-local! slot config)
|
||||
;; Print summary
|
||||
(print-summary! slot config)))
|
||||
|
||||
(defn- tear-down!
|
||||
"Find all containers for this worktree, kill/rm them, delete mise.local.toml."
|
||||
[]
|
||||
(check-docker!)
|
||||
(let [prefix (container-prefix)
|
||||
{:keys [out]} (shell/sh* {:quiet? true}
|
||||
"docker" "ps" "-a"
|
||||
"--filter" (str "name=^" prefix)
|
||||
"--format" "{{.Names}}")
|
||||
containers (when (seq out)
|
||||
(->> out
|
||||
(remove str/blank?)
|
||||
vec))]
|
||||
(if (seq containers)
|
||||
(do
|
||||
(println (c/yellow "Stopping containers:"))
|
||||
(doseq [cname containers]
|
||||
(println (c/red " Killing " cname))
|
||||
(kill-container! cname))
|
||||
(println (c/green "All containers stopped.")))
|
||||
(println (c/yellow "No containers found for prefix: " prefix)))
|
||||
;; Remove mise.local.toml
|
||||
(let [toml-path (str u/project-root-directory "/mise.local.toml")]
|
||||
(when (.exists (java.io.File. toml-path))
|
||||
(.delete (java.io.File. toml-path))
|
||||
(println (c/green "Removed " toml-path))))))
|
||||
|
||||
(defn- print-status!
|
||||
"Show running containers for this worktree."
|
||||
[]
|
||||
(check-docker!)
|
||||
(let [prefix (container-prefix)
|
||||
{:keys [out]} (shell/sh* {:quiet? true}
|
||||
"docker" "ps" "-a"
|
||||
"--filter" (str "name=^" prefix)
|
||||
"--format" "{{.Names}}\t{{.Status}}\t{{.Ports}}")
|
||||
lines (when (seq out)
|
||||
(->> out (remove str/blank?) vec))]
|
||||
(if (seq lines)
|
||||
(let [rows (mapv (fn [line]
|
||||
(let [[cname status ports] (str/split line #"\t" 3)]
|
||||
{:container cname
|
||||
:status (or status "")
|
||||
:ports (or ports "")}))
|
||||
lines)]
|
||||
(println (c/bold (c/green "Containers for " (worktree-name) ":")))
|
||||
(println)
|
||||
(t/table rows :style :unicode))
|
||||
(println (c/yellow "No containers found for " (worktree-name))))))
|
||||
|
||||
(defn dev-env!
|
||||
"Top-level dispatcher for dev-env command."
|
||||
[{:keys [options]}]
|
||||
(let [{:keys [down status]} options]
|
||||
(cond
|
||||
down (tear-down!)
|
||||
status (print-status!)
|
||||
:else (stand-up! (-> options
|
||||
(update :app-db #(or % "h2")))))))
|
||||
Reference in New Issue
Block a user