Developing kicad-library-manager — architecture & gotchas

Read this before extending the app. Everything here is a real lesson from the build.

Architecture

  • server.js — pure-Node http server (no native deps). Serves the SPA + a REST API. In-memory DB persisted to data/data.json (debounced). DB = { libs, backup, probe, sync }.
  • public/index.html — the entire frontend: one vanilla-JS SPA, dark Adom theme. Polls GET /api/state every ~3s and re-renders. Three modes over one parsed model.
  • cli.js — thin wrapper (serve, app, probe).
  • No build step. node server.js (port 7823, KLM_PORT).

Data model

libs = { symbolLibs[], footprintLibs[], models3d[], defaultFpLibs[] }

  • symbolLibs[]: { nick, path, header, inTable, isDefault, symbols[] }; symbol = { id:"nick::name", name, ref, value, footprint, fpNick, fpName, datasheet, mpn, mpnField, description, raw, pendingAdd?, pendingDelete?, pendingEdit? }. raw is the original (symbol …) block — edits are surgical (setSymProp / renameSymInBlock) so sync re-emits faithfully.
  • footprintLibs[]: { nick, path, inTable, isDefault, footprints[] }; footprint = { id, name, file, model, models[], usedBy[], useCount, added?, rawMod?, pendingDelete? }.
  • models3d[]: { id, name, path, folder, usedBy[], useCount, added?, _local?, pendingDelete? }.

The desktop bridge (most important)

Barrett's machine is Linux, HOME=/home/barrett-land. Always reach the filesystem through adom-desktop — never the container FS.

  • This bridge has NO read_file / write_file ("Unknown desktop command"). Read via shell_execute cat (text) or base64 -w0 (binary); write via send_files (lands in ~/Downloads) then mv. pull_file exists under files too.
  • shell_execute runs sh -c on the user's machine; $HOME, $DISPLAY etc. are the user's.
  • KiCad GUI bridge verbs (kicad_open_symbol_editor, kicad_install_symbol, kicad_screenshot_all) are Windows/Mac-only — they return "not implemented on 'linux'". There is no CLI way to open the standalone Symbol Editor at a library on Linux, so "open in KiCad" was dropped.
  • Server-restart trap: killing by pkill -f server.js matches the kill command's own shell. Kill node procs by walking /proc/<pid>/cmdline, or fuser -k 7823/tcp, in a SEPARATE command from the start. Start detached: setsid node server.js >log 2>&1 < /dev/null &.

Parsing KiCad files

  • .kicad_sym: parseSymbolLib walks top-level (symbol "…") blocks at depth 1 (nested units Name_0_1/Name_1_1 stay inside their parent). renameSymInBlock renames units first, then the exact top-level name — doing it the other way corrupts _copy_copy_copy.
  • .kicad_mod: only the (model …) ref is parsed at probe; full text fetched on demand for preview.
  • lib-tables: tolerant regex for (lib (name …)(uri …)); isStockUri flags KiCad default libs (${KICAD*_*_DIR}, /usr/share/kicad, etc.).
  • LCSC PN only: mpnOf reads only LCSC/JLC properties (or a C##### value); reDeriveMpn re-runs it on boot so rule changes apply without a re-probe.

Previews (kicad-cli on the desktop)

  • Symbol: write the current (symbol …) block to a temp .kicad_sym, kicad-cli sym export svg --symbol <name>, cat the SVG back. Recolor for the dark canvas (themeSymbolSvg): light body, teal pins, muted text, transparent fills.
  • Footprint: kicad-cli fp export svg --layers F.CrtYd,F.Fab,F.SilkS,F.Paste,F.Mask,F.Cu — kicad-cli honors the layer ORDER, so courtyard sits below silk and pads render on top of the gray lines. liftFootprintText then moves the <g class="stroked-text"> groups (pad numbers / ref / value) above everything so the numbers sit on the pads.
  • 3D: resolve the footprint's STEP model (expand ${KICAD*_3DMODEL_DIR}/usr/share/kicad/3dmodels), cat it from the desktop (STEP is ASCII), step2glb convert → GLB, serve at /api/model.glb (path ends .glb so Babylon detects the loader). Viewer is the Adom Babylon bundle loaded by dynamic import() (it's an ES module with relative chunk imports + CORS * — a classic <script> tag silently fails). All previews cached in-memory.
  • Live LCSC stock/price: /api/lcsc?part=C##### → the shared JLCPCB service (JLCPCB_API, default the john/service-jlcpcb URL); hover popover in symbol mode.

Pending model + sync

  • Delete flags (pendingDelete, toggles) instead of removing; add sets pendingAdd/added; edit sets pendingEdit. The header chip shows N new · N deleted · N changed. All flags live in DB.libs so they're covered by the undo snapshots.
  • Sync is a repeatable one-shot (no longer a stop/undo toggle): writes symbol libs (buildSymLibText excludes pendingDelete), writes added footprints/models, trashes flagged files to a reversible .klm-trash/, unregisters flagged whole libraries from the lib-tables, then pruneAfterSync drops deleted items + clears flags, and resets the button to idle.
  • Backup (immutable snapshot at probe) powers Revert. Undo/redo is a snapshot tree (HIST, in-memory; full-libs snapshots incl. pending-trash so undo reverses a queued delete).
  • ⚠️ Never clobber a user lib: writes go through .klm.bak backups; deletions are reversible (trash).

Publishing

adompkg publish --org adom. The files allowlist EXCLUDES data/ (the user's library data must never ship). Hero is a billboard composed from docs/shot.png.