app
service-kicad
Public Made by Adomby adom
Shared headless KiCad 10: DRC, ERC, SVG/Gerber/STEP export, Altium library conversion, symbol/footprint/3D-model lookup. Every Adom tool shells to this CLI instead of calling the HTTP API directly.
← Commit history
Publish 1.1.1
7 files changed
+172−13
SKILL.md+19−1@@ -8,7 +8,8 @@ description: > Trigger words: kicad service, service-kicad, kicad-cli remote, kicad DRC, kicad ERC, export gerbers, export step, board svg, schematic svg, altium to kicad, schlib, pcblib, kicad-packages3d,- shared kicad library, headless kicad.+ shared kicad library, headless kicad, kicad-cli, kicad-cli not found,+ install kicad, apt install kicad, need KiCad in the container, pcbnew. --- # service-kicad — shared headless KiCad@@ -17,6 +18,23 @@ A shared KiCad 10 service running on a dedicated container. Every Adom tool that needs KiCad operations (DRC, ERC, exports, library lookups) shells to the `service-kicad` CLI instead of installing KiCad locally. +## Never install KiCad in a container++There is no local KiCad in an Adom container, and an AI must never install one (`apt-get install kicad`,+pip/conda/flatpak KiCad, an AppImage) unless the user explicitly asks for a local install. A KiCad 7+installed by an AI on 2026-09-28 could not even read the team's KiCad 10 boards.++- **Just call `kicad-cli`.** This package installs a `kicad-cli` shim in `~/.local/bin`: plain DRC, ERC and+ exports go to service-kicad; anything else, or everything while the service is down, runs on the+ native `kicad-cli` of the user's own desktop (Windows or Mac, where they have KiCad anyway) through+ Adom Bridge. Inputs are sent over, `-o/--output` files come back. `KICAD_CLI_TARGET=<machine>` picks+ another desktop; `KICAD_CLI_PREFER=desktop` skips the service.+- **Service down?** `service-kicad health` answering 502 means the service container is down: say so, and+ use the shim (it falls back by itself). Do not install KiCad as a workaround.+- **Need pcbnew Python** (zone refill and save, scripted edits)? Run the desktop KiCad's own Python through+ Adom Bridge: `<KiCad>/10.0/bin/python.exe` on Windows (the KiCad bridge's `kicad_status` gives the+ install folder), with the files sent over and pulled back.+ ## EasyEDA logo: remove it during creation (always) 3D models that come from EasyEDA / LCSC (easyeda2kicad, LCSC `C` numbers, EasyEDA `.obj`, JLC libraries), and any board STEP/GLB that places those models, often carry an embossed **"LCEDA / EasyEDA" logo**: usually on a package side wall (seen on WS2812B-2020 and X0408FVS-100F-LPV01), sometimes on a chip top (seen on RP2350A and ADS1220), and sometimes the same colour as the body so it is easy to miss. It must not ship in anything we create or publish.
install.sh+10−1@@ -8,7 +8,8 @@ # keeps must be installed out of here. set -e PKG_DIR="$(cd "$(dirname "$0")" && pwd)"-VERSION="$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$PKG_DIR/package.json" | head -1)"+# The CLI binary is released separately from the package; 1.1.1 changed only the skill and the kicad-cli shim.+VERSION="${SERVICE_KICAD_BIN_VERSION:-1.1.0}" # 1. CLI binary → ~/.local/bin (on PATH for Adom containers, no sudo). BIN_DIR="$HOME/.local/bin"@@ -24,6 +25,14 @@ else exit 1 fi +# 1b. kicad-cli shim → ~/.local/bin/kicad-cli. Containers have NO local KiCad and must never install one:+# the shim runs service-kicad, and when the service is down, the native kicad-cli on the user's+# own desktop through Adom Bridge. ~/.local/bin is ahead of /usr/bin on PATH in Adom containers.+if [ -f "$PKG_DIR/kicad-cli-shim" ]; then+ install -m 0755 "$PKG_DIR/kicad-cli-shim" "$BIN_DIR/kicad-cli"+ echo "✓ kicad-cli shim → $BIN_DIR/kicad-cli (service-kicad, else the desktop's KiCad via Adom Bridge)"+fi+ # 2. Skill(s) → ~/.claude/skills/ SKILLS_DIR="$HOME/.claude/skills" mkdir -p "$SKILLS_DIR/service-kicad"
kicad-cli-shimadded+106@@ -0,0 +1,106 @@+#!/usr/bin/env bash+# kicad-cli for an Adom container: there is NO local KiCad here, and none should ever be installed.+# 1. service-kicad (the shared KiCad 10 service) when it is up and the command maps onto it;+# 2. otherwise the native kicad-cli on the user's own desktop (Windows or Mac) through Adom Bridge:+# input files are sent over, paths are rewritten, outputs (-o/--output) are pulled back.+# Env: KICAD_CLI_TARGET (default: the bridge's primary machine = the user's own), KICAD_CLI_THREAD,+# KICAD_CLI_PREFER=desktop to skip the service.+# Shipped by adom/service-kicad. Never `apt-get install kicad` in a container unless the user asks for a local install.+set -uo pipefail+TH="${KICAD_CLI_THREAD:-kicad-cli shim}"+say() { echo "kicad-cli (shim): $*" >&2; }+jq_py() { python3 -c "$1" "${@:2}"; }++if [[ $# -eq 0 || "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then+ cat <<'EOF'+kicad-cli (Adom shim): no local KiCad in this container. Commands run on+ 1. service-kicad (KiCad 10) when it is up and the command is a plain pcb drc / sch erc / pcb export step|gerbers|svg;+ 2. else the native kicad-cli on your own desktop through Adom Bridge (KiCad 10 there).+Use it exactly like kicad-cli, e.g. kicad-cli pcb drc --format json --output drc.json board.kicad_pcb+NEVER install KiCad in the container (apt kicad): use this, or `service-kicad` directly.+EOF+ exit 0+fi++# ---- 1. the service, for the commands it implements with no extra flags+try_service() {+ [[ $# -ge 2 ]] || return 1+ [[ "${KICAD_CLI_PREFER:-}" == "desktop" ]] && return 1+ command -v service-kicad >/dev/null || return 1+ local sub="$1 $2" fmt="" out="" file="" a+ shift 2+ case "$sub" in "pcb drc"|"sch erc"|"pcb export") ;; *) return 1;; esac+ local kind=""+ if [[ "$sub" == "pcb export" ]]; then kind="$1"; shift; case "$kind" in step|gerbers|svg) ;; *) return 1;; esac; fi+ while [[ $# -gt 0 ]]; do+ case "$1" in+ --format) fmt="$2"; shift 2;;+ --output|-o) out="$2"; shift 2;;+ -*) return 1;; # a flag the service does not take: use the desktop+ *) file="$1"; shift;;+ esac+ done+ [[ -f "$file" ]] || return 1+ service-kicad health >/dev/null 2>&1 || { say "service-kicad is down; using the desktop's KiCad"; return 1; }+ local cmd=(service-kicad ${sub})+ [[ -n "$kind" ]] && cmd+=("$kind")+ [[ -n "$fmt" && -z "$kind" ]] && cmd+=(--format "$fmt")+ [[ -n "$out" ]] && cmd+=(--out "$out")+ cmd+=("$file")+ say "service-kicad: ${cmd[*]:1}"+ "${cmd[@]}"; exit $?+}+try_service "$@"++# ---- 2. the user's desktop+T="${KICAD_CLI_TARGET:-$(adom-bridge targets 2>/dev/null | jq_py 'import sys,json;print(json.load(sys.stdin).get("primaryMachine",""))')}"+[[ -n "$T" ]] || { say "no service-kicad and no desktop connected through Adom Bridge. Do NOT install KiCad here: ask the user to connect their desktop, or report service-kicad down."; exit 2; }+AB=(adom-bridge --target "$T" --ai-thread "$TH")+CLI=$("${AB[@]}" kicad_status '{"reason":"find the native kicad-cli for the container kicad-cli shim"}' 2>/dev/null | jq_py 'import sys,json;d=json.load(sys.stdin);print((d.get("install") or {}).get("kicadCli",""))')+[[ -n "$CLI" ]] || { say "no KiCad found on $T. Do NOT install KiCad in the container; ask the user."; exit 2; }+WIN=0; [[ "$CLI" == *.exe || "$CLI" == [A-Za-z]:* ]] && WIN=1+if [[ $WIN == 1 ]]; then+ RTMP=$("${AB[@]}" shell_execute '{"command":"echo %TEMP%","reason":"temp folder for a kicad-cli job"}' | jq_py 'import sys,json;print(json.load(sys.stdin).get("stdout","").strip().replace(chr(92),"/"))')+else+ RTMP=$("${AB[@]}" shell_execute '{"command":"echo ${TMPDIR:-/tmp}","reason":"temp folder for a kicad-cli job"}' | jq_py 'import sys,json;print(json.load(sys.stdin).get("stdout","").strip().rstrip("/"))')+fi+job="kicad-cli-$(date +%s)-$$"; dest="$RTMP/$job"++# inputs: every existing local file argument, plus a board's / schematic's project siblings+declare -A MAP; files=(); args=(); outs=(); prev=""+for a in "$@"; do+ if [[ "$prev" == "-o" || "$prev" == "--output" ]]; then+ outs+=("$a"); args+=("$dest/out/$(basename "$a")"); prev="$a"; continue+ fi+ if [[ -f "$a" ]]; then+ f=$(readlink -f "$a"); files+=("$f"); args+=("$dest/$(basename "$f")")+ stem="${f%.*}"+ for ext in kicad_pro kicad_dru kicad_pcb kicad_sch kicad_prl; do [[ -f "$stem.$ext" && "$stem.$ext" != "$f" ]] && files+=("$stem.$ext"); done+ else+ args+=("$a")+ fi+ prev="$a"+done+if [[ ${#files[@]} -gt 0 ]]; then+ "${AB[@]}" send_files "$(jq_py 'import json,sys;print(json.dumps({"filePaths":sorted(set(sys.argv[2:])),"dest":sys.argv[1],"reason":"inputs for a kicad-cli job (no KiCad in the container)"}))' "$dest" "${files[@]}")" >/dev/null+fi+cmd=$(jq_py '+import json,sys+win=sys.argv[1]=="1"; cli=sys.argv[2]; dest=sys.argv[3]; a=sys.argv[4:]+q=lambda s: "\""+s+"\"" if (" " in s or "/" in s or "\\" in s) else s+fix=(lambda s: s.replace("/","\\")) if win else (lambda s: s)+mk=("mkdir \""+fix(dest+"/out")+"\" 2>nul & ") if win else ("mkdir -p \""+dest+"/out\" && ")+print(json.dumps({"command": mk+q(fix(cli))+" "+" ".join(q(fix(x)) if x.startswith(dest) else q(x) for x in a),"timeoutSeconds":600,"reason":"kicad-cli on the desktop (no KiCad in the container)"}))' "$WIN" "$CLI" "$dest" "${args[@]}")+say "running on $T: native KiCad"+res=$("${AB[@]}" shell_execute "$cmd")+echo "$res" | jq_py 'import sys,json;d=json.load(sys.stdin);o=d.get("stdout","");e=d.get("stderr","");sys.stdout.write(o);sys.stderr.write(e)'+code=$(echo "$res" | jq_py 'import sys,json;d=json.load(sys.stdin);print(d.get("exitCode",d.get("exit_code",0)) or 0)')+for o in "${outs[@]}"; do+ tmp=$(mktemp -d)+ "${AB[@]}" pull_file "$(jq_py 'import json,sys;print(json.dumps({"filePaths":[sys.argv[1]],"saveTo":sys.argv[2],"reason":"bring a kicad-cli output back"}))' "$dest/out/$(basename "$o")" "$tmp")" >/dev/null+ if [[ -e "$tmp/$(basename "$o")" ]]; then mkdir -p "$(dirname "$o")"; rm -rf "$o"; mv "$tmp/$(basename "$o")" "$o"; else say "output $o did not come back"; code=1; fi+ rm -rf "$tmp"+done+if [[ $WIN == 1 ]]; then rm_cmd="rmdir /s /q \"${dest//\//\\}\""; else rm_cmd="rm -rf \"$dest\""; fi+"${AB[@]}" shell_execute "$(jq_py 'import json,sys;print(json.dumps({"command":sys.argv[1],"reason":"clean up the kicad-cli job folder"}))' "$rm_cmd")" >/dev/null 2>&1 || true+exit "$code"
package.json+26−8@@ -1,6 +1,6 @@ { "slug": "service-kicad",- "version": "1.1.0",+ "version": "1.1.1", "type": "app", "description": "Shared headless KiCad 10: DRC, ERC, SVG/Gerber/STEP export, Altium library conversion, symbol/footprint/3D-model lookup. Every Adom tool shells to this CLI instead of calling the HTTP API directly.", "keywords": [@@ -14,7 +14,8 @@ "altium", "pcb", "schematic",- "cli"+ "cli",+ "pcbnew" ], "tags": [ "kicad",@@ -23,7 +24,7 @@ "cli", "shared-service" ],- "discovery_pitch": "Run KiCad operations (DRC, ERC, Gerber/SVG/STEP export, Altium conversion, library lookups) from any container with no local KiCad install — free shared infrastructure.",+ "discovery_pitch": "Run KiCad operations (DRC, ERC, Gerber/SVG/STEP export, Altium conversion, library lookups) from any container with no local KiCad install \u2014 free shared infrastructure.", "discovery_triggers": [ "run drc on my board", "export gerbers",@@ -33,13 +34,30 @@ "kicad without installing kicad", "headless kicad", "kicad footprint svg",- "service-kicad"+ "service-kicad",+ "kicad-cli not found",+ "install kicad",+ "apt install kicad",+ "need kicad-cli",+ "run kicad-cli in the container" ], "sample_prompts": [- { "label": "Run DRC", "prompt": "Run DRC on board.kicad_pcb and summarize the violations" },- { "label": "Export Gerbers", "prompt": "Export gerbers for board.kicad_pcb into ./gerbers/" },- { "label": "Board STEP", "prompt": "Export a STEP model of board.kicad_pcb" },- { "label": "Altium → KiCad", "prompt": "Convert part.PcbLib to a KiCad footprint" }+ {+ "label": "Run DRC",+ "prompt": "Run DRC on board.kicad_pcb and summarize the violations"+ },+ {+ "label": "Export Gerbers",+ "prompt": "Export gerbers for board.kicad_pcb into ./gerbers/"+ },+ {+ "label": "Board STEP",+ "prompt": "Export a STEP model of board.kicad_pcb"+ },+ {+ "label": "Altium \u2192 KiCad",+ "prompt": "Convert part.PcbLib to a KiCad footprint"+ } ], "install": { "binary_name": "service-kicad"
page.json+9−3@@ -1,6 +1,6 @@ { "slug": "service-kicad",- "version": "1.1.0",+ "version": "1.1.1", "type": "app", "description": "Shared headless KiCad 10: DRC, ERC, SVG/Gerber/STEP export, Altium library conversion, symbol/footprint/3D-model lookup. Every Adom tool shells to this CLI instead of calling the HTTP API directly.", "dependencies": {},@@ -24,7 +24,8 @@ "altium", "pcb", "schematic",- "cli"+ "cli",+ "pcbnew" ], "tags": [ "kicad",@@ -43,7 +44,12 @@ "kicad without installing kicad", "headless kicad", "kicad footprint svg",- "service-kicad"+ "service-kicad",+ "kicad-cli not found",+ "install kicad",+ "apt install kicad",+ "need kicad-cli",+ "run kicad-cli in the container" ], "sample_prompts": [ {
uninstall.sh+2@@ -4,3 +4,5 @@ rm -f "$HOME/.local/bin/service-kicad" rm -rf "$HOME/.claude/skills/service-kicad" rm -f "$HOME/.bash_completion.d/service-kicad" echo "Uninstalled service-kicad"+# the kicad-cli shim, only if it is ours+if grep -q "Shipped by adom/service-kicad" "$HOME/.local/bin/kicad-cli" 2>/dev/null; then rm -f "$HOME/.local/bin/kicad-cli"; fi
verification/test_report_cli.py