Files
Kill_LIFE/tools/hw/cad_stack.sh
T
L'électron rare 7d4ee627bf feat(tools): add cockpit TUIs, ops scripts, CAD tooling, Mistral agents
Cockpit (54 scripts):
- Intelligence/agent TUIs, Mistral agents/studio TUI, e2e agent tests
- Mesh health, sync preflight, dirtyset cleanup, SSH healthcheck
- Mascarade runtime health, incident registry, dispatch mesh
- Machine registry, log ops, product contract audit
- Yiacad refonte/backend/proofs/operator TUIs
- Sentinelle cron, daily alignment, operator summaries
- Fab package, PCB AI fab, artifact WMS index TUIs

Ops:
- Deploy mascarade tower runtime
- Full operator lane sync

CAD:
- KiCad yiacad plugin, host MCP integration
- CAD stack scripts

Other:
- Mistral finetune tools
- Evals framework
- Mesh contract checker
- Updated AI scripts (zeroclaw, integrations)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 00:21:13 +01:00

460 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/deploy/cad/docker-compose.yml"
VERBOSE=0
usage() {
cat <<'EOF'
Usage: tools/hw/cad_stack.sh <command> [args...]
Stack CAD/EDA locale intégrée à Kill_LIFE.
Commands:
up [services...] Start CAD helper containers
down Stop CAD helper containers
ps Show CAD container status
build [services...] Build local CAD images
doctor Print tool versions from the containers
doctor-mcp Print MCP runtime homes and run quick MCP smokes
kicad-cli <args...> Run kicad-cli with Kill_LIFE mounted as workspace
freecad-cmd <args...> Run FreeCADCmd with Kill_LIFE mounted as workspace
openscad <args...> Run OpenSCAD with Kill_LIFE mounted as workspace
pio <args...> Run PlatformIO with Kill_LIFE mounted as workspace
mcp [args...] Run the KiCad MCP server in stdio mode
help Show this help
Env:
CAD_WORKSPACE_DIR Workspace mounted at /workspace (default: Kill_LIFE root)
CAD_HOST_ROOT Parent path mounted 1:1 for the MCP container (default: parent of Kill_LIFE)
KICAD_DOCKER_IMAGE KiCad v10 image/tag override (default: kicad/kicad:nightly-full)
EOF
}
log() {
printf '[kill_life:cad] %s\n' "$*"
}
debug() {
if [ "$VERBOSE" -eq 1 ]; then
printf '[kill_life:cad][dbg] %s\n' "$*" >&2
fi
}
die() {
printf '[kill_life:cad][err] %s\n' "$*" >&2
exit 1
}
docker_available() {
command -v docker >/dev/null 2>&1 || return 1
if [[ -n "${DOCKER_HOST:-}" ]]; then
if [[ "$DOCKER_HOST" == unix://* ]]; then
local socket_path
socket_path="${DOCKER_HOST#unix://}"
[ -S "$socket_path" ] || return 1
fi
return 0
fi
[ -S "/Users/electron/.docker/run/docker.sock" ] || [ -S "/var/run/docker.sock" ] || [ -S "${HOME}/.docker/run/docker.sock" ] || return 1
}
compose() {
docker_available || die "Docker is required for this operation (compose unavailable)"
debug "docker compose -f $COMPOSE_FILE $*"
docker compose -f "$COMPOSE_FILE" "$@"
}
resolve_host_kicad_cli() {
if [ -n "${KICAD_CLI:-}" ] && [ -x "${KICAD_CLI}" ]; then
printf '%s' "$KICAD_CLI"
return 0
fi
if [ -x /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli ]; then
printf '%s' /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli
return 0
fi
command -v kicad-cli 2>/dev/null || true
}
resolve_host_freecad_cmd() {
if [ -n "${FREECAD_CMD:-}" ] && [ -x "${FREECAD_CMD}" ]; then
printf '%s' "$FREECAD_CMD"
return 0
fi
if [ -x /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd ]; then
printf '%s' /Applications/FreeCAD.app/Contents/Resources/bin/freecadcmd
return 0
fi
command -v freecadcmd 2>/dev/null || command -v FreeCADCmd 2>/dev/null || true
}
resolve_host_openscad() {
local -a candidates=()
local candidate=""
if [ -n "${OPENSCAD_BIN:-}" ] && [ -x "${OPENSCAD_BIN}" ]; then
candidates+=("${OPENSCAD_BIN}")
fi
if command -v openscad >/dev/null 2>&1; then
candidates+=("$(command -v openscad)")
fi
if [ -x /Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD ]; then
candidates+=(/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD)
fi
for candidate in "${candidates[@]}"; do
if "$candidate" --version >/dev/null 2>&1; then
printf '%s' "$candidate"
return 0
fi
done
return 1
}
ensure_service_up() {
local service="$1"
if ! docker_available; then
return 1
fi
if ! compose ps --status running --services | grep -qx "$service"; then
log "Starting $service"
compose up -d "$service"
fi
}
run_shell_as_host_user() {
local service="$1"
local home_dir="$2"
shift 2
local shell_command="$*"
compose exec -T \
--user "$(id -u):$(id -g)" \
-e HOME="$home_dir" \
"$service" \
sh -lc "$shell_command"
}
run_tool_as_host_user() {
local service="$1"
local home_dir="$2"
local prelude="$3"
local binary="$4"
shift 4
compose exec -T \
--user "$(id -u):$(id -g)" \
-e HOME="$home_dir" \
"$service" \
sh -lc 'set -e; mkdir -p "$HOME"; '"$prelude"'; exec "$@"' sh "$binary" "$@"
}
run_shell_in_service_user() {
local service="$1"
local home_dir="$2"
shift 2
local shell_command="$*"
compose exec -T \
-e HOME="$home_dir" \
"$service" \
sh -lc "$shell_command"
}
run_tool_in_service_user() {
local service="$1"
local home_dir="$2"
local prelude="$3"
local binary="$4"
shift 4
compose exec -T \
-e HOME="$home_dir" \
"$service" \
sh -lc 'set -e; mkdir -p "$HOME"; '"$prelude"'; exec "$@"' sh "$binary" "$@"
}
build_cmd() {
local restart_services=()
if [ "$#" -gt 0 ]; then
compose build "$@"
for service in "$@"; do
case "$service" in
kicad-headless|freecad-headless|openscad-headless|platformio)
restart_services+=("$service")
;;
esac
done
else
compose build
restart_services=(kicad-headless freecad-headless openscad-headless platformio)
fi
if [ "${#restart_services[@]}" -gt 0 ]; then
log "Recreating updated services: ${restart_services[*]}"
compose up -d --force-recreate "${restart_services[@]}"
fi
}
up_cmd() {
if [ "$#" -gt 0 ]; then
compose up -d "$@"
return
fi
compose up -d kicad-headless freecad-headless openscad-headless platformio
}
doctor_cmd() {
local host_kicad=""
local host_freecad=""
local host_openscad=""
local host_platformio=""
local degraded=0
host_kicad="$(resolve_host_kicad_cli)"
host_freecad="$(resolve_host_freecad_cmd)"
host_openscad="$(resolve_host_openscad)"
host_platformio="$(command -v platformio 2>/dev/null || true)"
if [ -z "$host_kicad" ]; then
if docker_available; then
ensure_service_up kicad-headless
else
log "WARN: kicad-cli non trouvé et docker indisponible, check kicad ignoré"
degraded=1
fi
fi
if [ -z "$host_freecad" ]; then
if docker_available; then
ensure_service_up freecad-headless
else
log "WARN: freecadcmd non trouvé et docker indisponible, check freecad ignoré"
degraded=1
fi
fi
if [ -z "$host_openscad" ]; then
if docker_available; then
ensure_service_up openscad-headless
else
log "WARN: openscad non trouvé et docker indisponible, check openscad ignoré"
degraded=1
fi
fi
if [ -n "$host_kicad" ]; then
"$host_kicad" version
elif docker_available; then
run_shell_as_host_user \
kicad-headless \
/workspace/.cad-home/kicad-headless \
'mkdir -p "$HOME" && kicad-cli version'
fi
if [ -n "$host_freecad" ]; then
"$host_freecad" -c 'import FreeCAD; print(".".join(FreeCAD.Version()[:3]))'
elif docker_available; then
run_shell_in_service_user \
freecad-headless \
/workspace/.cad-home/freecad-headless \
'mkdir -p "$HOME" && freecadcmd -c "import FreeCAD; print(\".\".join(FreeCAD.Version()[:3]))"'
fi
if [ -n "$host_openscad" ]; then
"$host_openscad" --version
elif docker_available; then
run_shell_as_host_user \
openscad-headless \
/workspace/.cad-home/openscad-headless \
'mkdir -p "$HOME" && openscad --version'
fi
if [ -n "$host_platformio" ]; then
"$host_platformio" --version
elif docker_available; then
ensure_service_up platformio
run_shell_as_host_user \
platformio \
/workspace/.cad-home/platformio \
'mkdir -p "$HOME" "$HOME/.platformio" && export PLATFORMIO_CORE_DIR="$HOME/.platformio" && pio --version'
else
log "WARN: platformio non trouvé et docker indisponible, check platformio ignoré"
degraded=1
fi
if [ "$degraded" -eq 0 ]; then
log "OK: CAD doctor checks executed successfully."
else
log "DEGRADED: CAD doctor fallback host-only for unavailable container runtime."
fi
}
doctor_mcp_cmd() {
"$ROOT_DIR/tools/run_freecad_mcp.sh" --doctor
"$ROOT_DIR/tools/run_openscad_mcp.sh" --doctor
(
cd "$ROOT_DIR"
python3 tools/freecad_mcp_smoke.py --json --quick
python3 tools/openscad_mcp_smoke.py --json --quick
)
}
kicad_cli_cmd() {
local host_kicad=""
host_kicad="$(resolve_host_kicad_cli)"
if [ -n "$host_kicad" ]; then
"$host_kicad" "$@"
return
fi
ensure_service_up kicad-headless
run_tool_as_host_user \
kicad-headless \
/workspace/.cad-home/kicad-headless \
":" \
kicad-cli \
"$@"
}
freecad_cmd() {
local host_freecad=""
host_freecad="$(resolve_host_freecad_cmd)"
if [ -n "$host_freecad" ]; then
"$host_freecad" "$@"
return
fi
ensure_service_up freecad-headless
run_tool_in_service_user \
freecad-headless \
/workspace/.cad-home/freecad-headless \
":" \
freecadcmd \
"$@"
}
openscad_cmd() {
local host_openscad=""
host_openscad="$(resolve_host_openscad)"
if [ -n "$host_openscad" ]; then
"$host_openscad" "$@"
return
fi
ensure_service_up openscad-headless
run_tool_as_host_user \
openscad-headless \
/workspace/.cad-home/openscad-headless \
":" \
openscad \
"$@"
}
pio_cmd() {
ensure_service_up platformio
run_tool_as_host_user \
platformio \
/workspace/.cad-home/platformio \
'mkdir -p "$HOME/.platformio"; export PLATFORMIO_CORE_DIR="$HOME/.platformio"' \
pio \
"$@"
}
mcp_cmd() {
"$ROOT_DIR/tools/hw/run_kicad_mcp.sh" "$@"
}
while [ "$#" -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
break
;;
esac
done
CMD="${1:-help}"
if [ "$#" -gt 0 ]; then
shift
fi
[ -f "$COMPOSE_FILE" ] || die "CAD compose file not found: $COMPOSE_FILE"
export CAD_WORKSPACE_DIR="${CAD_WORKSPACE_DIR:-$ROOT_DIR}"
export CAD_HOST_ROOT="${CAD_HOST_ROOT:-$(cd "$ROOT_DIR/.." && pwd)}"
if [ "$CMD" = "help" ]; then
usage
exit 0
fi
case "$CMD" in
up|down|ps|build|doctor|doctor-mcp|kicad-cli|freecad-cmd|openscad|pio|mcp)
;;
*)
usage >&2
die "unknown command: $CMD"
;;
esac
debug "root=$ROOT_DIR"
debug "compose_file=$COMPOSE_FILE"
debug "workspace_dir=$CAD_WORKSPACE_DIR"
debug "command=$CMD"
case "$CMD" in
up)
up_cmd "$@"
;;
down)
compose down
;;
ps)
compose ps
;;
build)
build_cmd "$@"
;;
doctor)
doctor_cmd
;;
doctor-mcp)
doctor_mcp_cmd
;;
kicad-cli)
if [ "$#" -eq 0 ]; then
echo "cad_stack.sh kicad-cli: missing arguments" >&2
exit 2
fi
kicad_cli_cmd "$@"
;;
freecad-cmd)
if [ "$#" -eq 0 ]; then
echo "cad_stack.sh freecad-cmd: missing arguments" >&2
exit 2
fi
freecad_cmd "$@"
;;
openscad)
if [ "$#" -eq 0 ]; then
echo "cad_stack.sh openscad: missing arguments" >&2
exit 2
fi
openscad_cmd "$@"
;;
pio)
if [ "$#" -eq 0 ]; then
echo "cad_stack.sh pio: missing arguments" >&2
exit 2
fi
pio_cmd "$@"
;;
mcp)
mcp_cmd "$@"
;;
esac