app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
Sync current source (open-source, up to date)
14 files changed
+708−85
Cargo.lockadded+321@@ -0,0 +1,321 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "adom-lbr"+version = "0.1.4"+dependencies = [+ "anyhow",+ "clap",+ "clap_complete",+ "quick-xml",+ "regex-lite",+ "serde",+ "serde_json",+ "tiny_http",+]++[[package]]+name = "anstream"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"+dependencies = [+ "anstyle",+ "anstyle-parse",+ "anstyle-query",+ "anstyle-wincon",+ "colorchoice",+ "is_terminal_polyfill",+ "utf8parse",+]++[[package]]+name = "anstyle"+version = "1.0.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"++[[package]]+name = "anstyle-parse"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"+dependencies = [+ "utf8parse",+]++[[package]]+name = "anstyle-query"+version = "1.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"+dependencies = [+ "windows-sys",+]++[[package]]+name = "anstyle-wincon"+version = "3.0.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"+dependencies = [+ "anstyle",+ "once_cell_polyfill",+ "windows-sys",+]++[[package]]+name = "anyhow"+version = "1.0.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"++[[package]]+name = "ascii"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"++[[package]]+name = "chunked_transfer"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"++[[package]]+name = "clap"+version = "4.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"+dependencies = [+ "clap_builder",+ "clap_derive",+]++[[package]]+name = "clap_builder"+version = "4.6.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"+dependencies = [+ "anstream",+ "anstyle",+ "clap_lex",+ "strsim",+]++[[package]]+name = "clap_complete"+version = "4.6.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"+dependencies = [+ "clap",+]++[[package]]+name = "clap_derive"+version = "4.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"+dependencies = [+ "heck",+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "clap_lex"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"++[[package]]+name = "colorchoice"+version = "1.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"++[[package]]+name = "heck"+version = "0.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"++[[package]]+name = "httpdate"+version = "1.0.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"++[[package]]+name = "is_terminal_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "log"+version = "0.4.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"++[[package]]+name = "memchr"+version = "2.8.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"++[[package]]+name = "once_cell_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"++[[package]]+name = "proc-macro2"+version = "1.0.106"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quick-xml"+version = "0.36.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"+dependencies = [+ "memchr",+ "serde",+]++[[package]]+name = "quote"+version = "1.0.45"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "regex-lite"+version = "0.1.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"++[[package]]+name = "serde"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"+dependencies = [+ "serde_core",+ "serde_derive",+]++[[package]]+name = "serde_core"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "serde_json"+version = "1.0.149"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "strsim"+version = "0.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"++[[package]]+name = "syn"+version = "2.0.117"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "tiny_http"+version = "0.12.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"+dependencies = [+ "ascii",+ "chunked_transfer",+ "httpdate",+ "log",+]++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "utf8parse"+version = "0.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"++[[package]]+name = "windows-link"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"++[[package]]+name = "windows-sys"+version = "0.61.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"+dependencies = [+ "windows-link",+]++[[package]]+name = "zmij"+version = "1.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
README.md+123−49@@ -1,87 +1,161 @@ # adom-lbr -**One part → every major EDA tool. A library generator, linter, and converter that takes your part and delivers to Fusion 360, Altium, OrCAD and KiCad — standalone Rust, no local EAGLE.**+EAGLE .lbr generator, linter, validator, and KiCad converter. Converts between KiCad `.kicad_sym` and EAGLE `.lbr` formats so both Fusion 360 and KiCad users can share component libraries. -adom-lbr turns a `.kicad_sym` (and optional `.kicad_mod`) into a true EAGLE `.lbr` with a complete deviceset — symbol + package + pin→pad connects. Because EAGLE libraries are the universal interchange format, that one `.lbr` imports cleanly into **Fusion 360 Electronics, Altium, and OrCAD**; and for KiCad it round-trips back to a native `.kicad_sym`. It can also synthesize a placeable symbol from a footprint's pads when there's no schematic symbol, and verify a library through `service-kicad`. Standalone Rust, no Node, no gallia.+ -## Formats & targets+ -| Direction | Format | Used by |-|---|---|---|-| **Generate** | EAGLE `.lbr` (symbol + package + connects) | the source of truth adom-lbr builds |-| **Deliver → Fusion 360** | `.lbr` imported directly | Autodesk Fusion 360 Electronics |-| **Deliver → Altium** | `.lbr` import | Altium Designer |-| **Deliver → OrCAD** | `.lbr` import | Cadence OrCAD / Allegro |-| **Deliver → KiCad** | converted to native `.kicad_sym` | KiCad 6+ |-| **Import (reverse)** | EAGLE `.lbr` → `.kicad_sym` | bring EAGLE parts into KiCad |+Standalone Rust — no Node, no gallia, no local EAGLE/Fusion install. Ships a+live, AI-drivable lint-report app (`adom-lbr serve`) alongside the CLI. -The **Send to** bar in the live app pushes the loaded library to whichever of those tools you're running (via the Adom Desktop bridge); the Altium/OrCAD bridges are an open contribution slot.+## Why this exists -## The 3-column library viewer — and an interactive embed+Fusion 360 Electronics shows "No Components" when an `.lbr` file has an empty `<packages>` section or empty `<connects/>`. The old JS generator produced these broken files. adom-lbr generates a **complete** deviceset: symbol + placeholder SMD package + full pin-to-pad connect mappings. -+KiCad users can import any `.lbr` back to `.kicad_sym` with `adom-lbr import`, validated locally (Rust parser). -▶ **[Open the live ADS1115 3-column viewer](https://wiki.adom.inc/api/v1/pages/john/adom-lbr/files/demo/ads1115-viewer.html)** — the exact self-contained file `adom-lbr embed` produces, served straight from this page (pan/zoom each panel; the pin→pad table is fully populated from the datasheet). It is built to sit in an `<iframe>` once the wiki enables component-page iframe embedding.+## Install +```+adompkg install adom-lbr+``` -`adom-lbr embed` exports a **Fusion 360-style three-column viewer** as one self-contained `.html` file: **symbol** in column 1, **footprint** in column 2, the **3D chip** in column 3 — each independently pan/zoomable — with the full **pin → pad mapping table** below, carrying each pin's **type and datasheet description**. It's built to drop straight into an `<iframe>` on a **wiki component page**: a single portable file, no server, no external assets.+## Quick start ```bash-adom-lbr embed --dir <chip-dir> # → <mpn>-lbr-embed.html+# Fusion user: KiCad symbol to EAGLE .lbr+adom-lbr generate --sym RP2040.kicad_sym -o RP2040.lbr++# KiCad user: EAGLE .lbr to KiCad symbol+adom-lbr import RP2040.lbr -o RP2040.kicad_sym+adom-lbr check RP2040.kicad_sym++# Lint any .lbr for Fusion compatibility+adom-lbr lint suspicious.lbr++# Run the live lint-report app (AI-drivable: /state, /load, /eval, /console)+adom-lbr serve --port 8784 --file RP2040.lbr ```-```html-<iframe src="…/<mpn>-lbr-embed.html" width="100%" height="640" style="border:0"></iframe>++## Screenshots++### RP2040 in Fusion 360 Electronics++The generated `.lbr` opens in Fusion with all 57 pins, complete deviceset, and the symbol rendering correctly. The device table shows the package and connect mappings that make Fusion recognize the component.++### Linting the old broken .lbr++The linter catches the exact bugs that cause Fusion to show "No Components":++```json+{+ "passed": false,+ "errors": [+ {"code": "empty-packages", "message": "Packages section is empty"},+ {"code": "empty-connects", "message": "Device has no pin-to-pad connect mappings"},+ {"code": "device-no-package", "message": "Device has no package attribute"}+ ]+} ``` -It assembles the three renders, the pin→pad connects, and the datasheet pin descriptions from a chip directory — exactly the part data adom-symbol, adom-footprint, ds2sf and chip-thumbnailer already produce.+### Round-trip into KiCad 10++The same `.lbr` imported back to `.kicad_sym`, validated locally, and installed as a KiCad 10 symbol library. Renders with all pins, body rectangle, and description.++## Dependencies -## The library at a glance+### adompkg (auto-installed) -+None — standalone Rust binary. -Load an `.lbr` and the viewer breaks it down at a glance — **symbols, packages, devicesets, pins, pads** — and re-lints it server-side. A green **Lint passed** badge means the library is ready to import into Fusion 360 Electronics with no surprises. The top bar carries the **Send to** targets (KiCad / Fusion / Altium / OrCAD).+### System (manual) -## The linter catches what Fusion rejects+| Dependency | Required? | Purpose |+|---|---|---|+| service-kicad | Optional | Remote validation of `.kicad_sym` via SVG export (no local KiCad install needed) |+| adom-desktop | Optional | Used by adom-symbol's Send to for Fusion 360 delivery (not yet on adompkg) | -+**Upstream**: adom-symbol creates the `.kicad_sym` input. chip-fetcher downloads `.lbr` files from manufacturer sites. -This is the whole point. Fusion will silently show "No Components" or refuse to place a part when a library is subtly broken — adom-lbr catches it first. Here the deviceset has **no pin-to-pad connect mappings**, so the linter flags `empty-connects` with a plain-English reason: *"Fusion cannot use this component."* It also catches missing packages, devices with no package attribute, malformed XML, and invisible symbol bodies — each with a code and a fix.+**Downstream**: generated `.lbr` opens directly in Fusion 360 Electronics. Imported `.kicad_sym` installs into KiCad via `kicad_install_library`. -## Generate from the command line+## Features -+- Bidirectional conversion: `.kicad_sym` to `.lbr` and `.lbr` to `.kicad_sym`+- Generates complete EAGLE `.lbr` with symbol + placeholder SMD package + connect mappings+- Handles duplicate pin names (IOVDD x6, DVDD x2) with EAGLE `@N` suffix convention+- 7-check linter: empty-packages, empty-connects, device-no-package, duplicate-pin-name, connect-pad-missing, malformed-xml, no-symbol-layer+- JSON lint output for programmatic use+- `check` validates `.kicad_sym` locally (Rust parser)+- Shell completions (bash/zsh/fish) -`generate` builds the whole library in one shot from a `.kicad_sym` (+ optional `.kicad_mod`), lint-checks it, and writes a native `.lbr`. The same engine powers the **Send to → Fusion/Altium/OrCAD** flow in adom-symbol and adom-footprint, so whatever you see in those apps is exactly what lands in your library.+## Talk to the AI -## What it does+**Generate:**+- "Generate an lbr from RP2040.kicad_sym"+- "Convert this KiCad symbol to EAGLE format"+- "Make a Fusion 360 library from this symbol" -- **`generate`** — `.kicad_sym` (+ `.kicad_mod`) → native `.lbr`, lint-checked for Fusion compatibility.-- **`lint` / `validate`** — catch the things Fusion rejects: missing connects, incomplete devicesets, bad pad maps.-- **`import`** — EAGLE `.lbr` → KiCad `.kicad_sym`.-- **Live app** — a viewer that lints the loaded `.lbr` and shows symbols / packages / devicesets / pins / pads at a glance.+**Import:**+- "Import this .lbr to KiCad format"+- "Convert EAGLE library to .kicad_sym" -## Send to+**Lint:**+- "Lint this .lbr file"+- "Check if this library will work in Fusion"+- "Why does Fusion show No Components?" -Fusion 360 (open the `.lbr` directly), Altium / OrCAD (download the `.lbr` + import steps; native exporters on the roadmap), KiCad (converted back to `.kicad_sym`).+## Skills -## Open source — check it out, vibe-code it, send it back+| Skill | Purpose |+|---|---|+| adom-lbr | Generate, import, lint, validate, check EAGLE .lbr / KiCad .kicad_sym files | -adom-lbr is MIT and **fully open**. The complete Rust source is in this page's **Files** tab — browse it, or grab `src/` + `Cargo.toml`.+## CLI reference -1. **Build it.** `cargo build --release` drops a standalone binary (no Node, no gallia).-2. **Point your install at your checkout.** `adompkg link john/adom-lbr ./adom-lbr` so the `adom-lbr` you run is your local copy.-3. **Vibe-code a change.** Open the folder in Claude Code and describe what you want — a new lint rule, another export target, a smarter symbol synthesizer. The lint rules live in one readable list, easy to extend.-4. **Send it back.** Open a **pull request** on this page (the wiki reviews and merges per-repo PRs), or start a discussion thread. Good additions get folded into the shipped app.+| Command | Description |+|---|---|+| `generate --sym <file> [-o out] [--fp <file>] [--name <name>]` | KiCad `.kicad_sym` to EAGLE `.lbr` |+| `import <file> [-o out]` | EAGLE `.lbr` to KiCad `.kicad_sym` |+| `lint <file>` | Lint `.lbr`, output JSON with error codes |+| `validate <file>` | Validate `.lbr`, human-readable output |+| `check <file>` | Validate `.kicad_sym` locally (structure, pins, properties) |+| `serve [--port 8784] [--file <lbr>]` | Run the live AI-drivable lint-report app |+| `health` | Print version |+| `install` | Install Claude Code skill + shell completions |+| `completions <shell>` | Generate bash/zsh/fish completions | -## Part of the Adom EDA family+The live app (`serve`) follows the Adom app-creator contract: `GET /state`,+`POST /load` (push raw `.lbr` XML — it's re-linted server-side), `POST /eval` ++`GET /eval/pending` + `POST /eval/:id/result`, `GET/POST /console`,+`POST /shutdown`, `GET /favicon.svg`, and best-effort HD port registration. -Used by **chip-fetcher** and the Send-to flow across **adom-symbol** and **adom-footprint**; pairs with **ds2sf**.+## Build from source -## Run it+The full source ships on the wiki app page — anyone in the community can build+it locally: ```bash-adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --name PART --output part.lbr-adom-lbr serve --port 8784 # live linter app (AI-drivable)-adom-lbr embed --dir <chip-dir> # Fusion-style 3-column viewer as one .html+adompkg source adom-lbr # downloads + unpacks the repo+cargo build --release # standalone — only needs Rust + cargo+cp target/release/adom-lbr ~/.local/bin/ ```-MIT, contributions welcome.++No runtime dependencies — `generate`, `import`, `lint`, `validate`, and `check`+all run fully locally in Rust.++## Contribute back via pull request++adom-lbr is public, source included. To propose a change:++1. `adompkg source adom-lbr` to get the repo.+2. Make your change and `cargo build --release` to confirm it compiles.+3. Open a pull request against the wiki page for `adom-lbr`.++> **Note:** real PR support on wiki.adom.inc is in progress. Until it lands,+> send patches or a branch link to the maintainers and they will apply it.++## Wiki++https://wiki.adom.inc/apps/adom-lbr
VERSIONadded+1@@ -0,0 +1 @@+0.1.4
bin/adom-lbr⋯ 1 unchanged line ⋯
docs/icon.svgadded+1@@ -0,0 +1 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4H9a1.5 1.5 0 0 1 1.5 1.5V20a1.5 1.5 0 0 0-1.5-1.5H5.5A1.5 1.5 0 0 1 4 17z"/><path d="M20 5.5A1.5 1.5 0 0 0 18.5 4H15a1.5 1.5 0 0 0-1.5 1.5V20a1.5 1.5 0 0 1 1.5-1.5h3.5a1.5 1.5 0 0 0 1.5-1.5z"/></svg>
docs/screenshot-fusion.pngadded⋯ 1 unchanged line ⋯
docs/screenshot-lint.pngadded⋯ 1 unchanged line ⋯
install.sh+12−2@@ -1,4 +1,14 @@ #!/bin/bash+# install.sh for adom-lbr (app).+# Runs from the extracted module directory (~/project/adom_modules/adom-lbr/). set -euo pipefail-DIR="$(cd "$(dirname "$0")" && pwd)"; BIN="${HOME}/.local/bin"; mkdir -p "$BIN"-install -m 0755 "$DIR/bin/adom-lbr" "$BIN/adom-lbr"; echo "Installed adom-lbr"++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++# App install: link the binary onto PATH.+BIN_DIR="${HOME}/.local/bin"+mkdir -p "$BIN_DIR"+install -m 0755 "$DIR/bin/adom-lbr" "$BIN_DIR/adom-lbr"+echo "Installed adom-lbr -> $BIN_DIR/adom-lbr"+
package.json+3−19@@ -1,28 +1,12 @@ { "slug": "adom-lbr",- "version": "1.0.3",+ "version": "0.1.4", "type": "app",- "description": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.",+ "description": "EAGLE .lbr generator, 7-check linter, and bidirectional KiCad converter", "dependencies": {},- "tags": [- "eda",- "kicad",- "eagle",- "lbr",- "fusion360",- "viewer"- ],- "hero": {- "type": "billboard",- "headline": "See the part, three ways.",- "subhead": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.",- "screenshot": "screenshots/shot.png",- "accent": "#00b8b1"- }, "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" },- "needs_sudo": false,- "license": "MIT"+ "needs_sudo": false }
release.shadded+87@@ -0,0 +1,87 @@+#!/bin/bash+#+# Canonical release.sh for an adompkg source repo.+#+# Reads version from package.json, optionally bumps it (--bump patch|minor|major),+# runs build.sh if present, then runs `adompkg publish` and prints the resulting+# package URL.+#+# Usage:+# ADOMPKG_TOKEN=<carbon-api-key> ./release.sh # publish current version+# ADOMPKG_TOKEN=<carbon-api-key> ./release.sh --bump patch # bump + publish+#+set -euo pipefail++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++if [ ! -f package.json ]; then+ echo "release.sh: no package.json found in $DIR" >&2+ exit 1+fi++REGISTRY="${ADOMPKG_REGISTRY:-https://git-wiki-ktqxite5iglh.adom.cloud}"++if [ -z "${ADOMPKG_TOKEN:-}" ]; then+ if [ -r /var/run/adom/api-key ]; then+ ADOMPKG_TOKEN="$(cat /var/run/adom/api-key)"+ export ADOMPKG_TOKEN+ else+ echo "release.sh: ADOMPKG_TOKEN is required (set ADOMPKG_TOKEN=<carbon-api-key>)" >&2+ exit 1+ fi+fi++# Read current version.+CUR_VERSION=$(node -e "console.log(require('./package.json').version)")+SLUG=$(node -e "console.log(require('./package.json').slug || require('./package.json').name)")++BUMP=""+for a in "$@"; do+ case "$a" in+ --bump)+ shift+ BUMP="${1:-patch}"+ shift || true+ ;;+ --bump=*)+ BUMP="${a#--bump=}"+ shift+ ;;+ esac+done++if [ -n "$BUMP" ]; then+ NEXT=$(node -e "+ const [maj, min, pat] = process.argv[1].split('.').map(n => parseInt(n, 10));+ const b = process.argv[2];+ let out;+ if (b === 'major') out = (maj + 1) + '.0.0';+ else if (b === 'minor') out = maj + '.' + (min + 1) + '.0';+ else out = maj + '.' + min + '.' + (pat + 1);+ console.log(out);+ " "$CUR_VERSION" "$BUMP")+ echo "release.sh: bumping $CUR_VERSION -> $NEXT ($BUMP)"+ node -e "+ const fs = require('fs');+ const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));+ p.version = process.argv[1];+ fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');+ " "$NEXT"+ CUR_VERSION="$NEXT"+fi++# Optional build step.+if [ -x ./build.sh ]; then+ echo "release.sh: running build.sh"+ ./build.sh+fi++# Publish.+echo "release.sh: publishing $SLUG@$CUR_VERSION to $REGISTRY"+adompkg publish .++# Print published URL.+echo+echo "Published: $REGISTRY/pages/$SLUG"+echo "Install: adompkg install $SLUG"
src/main.rs+67−13@@ -1,3 +1,4 @@+mod provenance; mod deliver; mod server; @@ -989,6 +990,17 @@ fn main() { process::exit(1); } }+ // Build the 3-column (symbol|footprint|3D) viewer from the part dir+ // (the .lbr's parent), so GET / shows the full studio, not just lint.+ if let Some(dir) = f.parent() {+ match build_embed_html(dir) {+ Ok((html, pins)) => {+ eprintln!("[adom-lbr] 3-column viewer ready ({pins} pins) from {}", dir.display());+ server::set_embed(html);+ }+ Err(e) => eprintln!("[adom-lbr] no 3-column viewer ({e}); serving lint card"),+ }+ } } if let Err(e) = server::run(port) { eprintln!("ERROR: {e:#}");@@ -1062,34 +1074,59 @@ fn read_first(dir: &std::path::Path, names: &[String]) -> Option<String> { } fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> {+ let mpn = dir.file_name().and_then(|s| s.to_str()).ok_or("bad --dir")?.to_string();+ let (html, pin_count) = build_embed_html(dir)?;+ let out = out.unwrap_or_else(|| dir.join(format!("{mpn}-lbr-embed.html")));+ fs::write(&out, &html).map_err(|e| format!("writing {}: {e}", out.display()))?;+ println!("OK: 3-column interactive embed ({pin_count} pins) → {}", out.display());+ Ok(())+}++/// Build the 3-column (symbol | footprint | 3D) interactive HTML for a part dir.+/// Reads pre-rendered assets, tolerating both `<mpn>-` prefixed and generic+/// (`symbol.svg`, `assets/footprint.svg`, …) names so the same renders that+/// drive the wiki page also drive the live app and the `embed` export.+pub fn build_embed_html(dir: &std::path::Path) -> Result<(String, usize), String> { let mpn = dir.file_name().and_then(|s| s.to_str()).ok_or("bad --dir")?.to_string(); // renders- let sym_svg = read_first(dir, &[format!("{mpn}-symbol.svg")])+ let sym_svg = read_first(dir, &[format!("{mpn}-symbol.svg"), "symbol.svg".into(), "assets/symbol.svg".into()]) .map(|s| s) .unwrap_or_else(|| "<div class=\"ph\">no symbol render in this directory</div>".into());- let fp_svg = read_first(dir, &[format!("{mpn}-footprint.authoritative.svg"), format!("{mpn}-footprint.svg")])+ let fp_svg = read_first(dir, &[format!("{mpn}-footprint.authoritative.svg"), format!("{mpn}-footprint.svg"), "footprint.svg".into(), "assets/footprint.svg".into()]) .unwrap_or_else(|| "<div class=\"ph\">no footprint render in this directory</div>".into()); let threed = {- let cands = ["-3d-iso-named.png", "-3d-iso.png", "-3d-iso-lg.png"]- .iter().map(|s| dir.join(format!("{mpn}{s}"))).find(|p| p.exists());+ let names = [+ format!("{mpn}-3d-iso-named.png"), format!("{mpn}-3d-iso.png"), format!("{mpn}-3d-iso-lg.png"),+ "3d-iso.png".into(), "assets/3d-iso.png".into(),+ ];+ let cands = names.iter().map(|s| dir.join(s)).find(|p| p.exists()); match cands.and_then(|p| fs::read(&p).ok()) { Some(bytes) => format!("<img src=\"data:image/png;base64,{}\">", b64(&bytes)), None => "<div class=\"ph\">no 3D render in this directory</div>".into(), } };- // optional pin→pad connects from a .lbr+ // optional pin→pad connects from a .lbr (tolerate generic name) let mut pin_to_pad: std::collections::HashMap<String, String> = std::collections::HashMap::new();- if let Some(lbr) = read_first(dir, &[format!("{mpn}.lbr")]) {+ let mut connect_order: Vec<(String, String)> = Vec::new();+ let lbr_text = read_first(dir, &[format!("{mpn}.lbr"), "library.lbr".into()])+ .or_else(|| {+ // any *.lbr in the dir+ fs::read_dir(dir).ok().and_then(|rd| rd.filter_map(|e| e.ok())+ .find(|e| e.path().extension().map(|x| x == "lbr").unwrap_or(false))+ .and_then(|e| fs::read_to_string(e.path()).ok()))+ });+ if let Some(lbr) = &lbr_text { if let Ok(re) = regex_lite::Regex::new(r#"<connect gate="[^"]*" pin="([^"]+)" pad="([^"]+)"/>"#) {- for cap in re.captures_iter(&lbr) {+ for cap in re.captures_iter(lbr) { pin_to_pad.insert(cap[1].to_string(), cap[2].to_string());+ connect_order.push((cap[1].to_string(), cap[2].to_string())); } } } // pins + descriptions from the extracted symbol JSON let mut pins_json = String::from("[]"); let mut pad_count = fp_svg.matches("adom-pad").count().max(0);- if let Some(ext) = read_first(dir, &[format!("{mpn}-symbol.extracted.json")]) {+ if let Some(ext) = read_first(dir, &[format!("{mpn}-symbol.extracted.json"), "symbol.extracted.json".into(), "assets/symbol.extracted.json".into()]) { if let Ok(v) = serde_json::from_str::<serde_json::Value>(&ext) { if let Some(arr) = v.get("pins").and_then(|p| p.as_array()) { let rows: Vec<serde_json::Value> = arr.iter().map(|p| {@@ -1112,19 +1149,36 @@ fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> } } }+ // fallback: no extracted JSON — build the pin→pad rows straight from the .lbr connects+ if pins_json == "[]" && !connect_order.is_empty() {+ let rows: Vec<serde_json::Value> = connect_order.iter().map(|(pin, pad)| {+ serde_json::json!({ "number": pad, "name": pin, "pad": pad, "type": "Signal", "color": "#80cbc4", "desc": "" })+ }).collect();+ pad_count = pad_count.max(rows.len());+ pins_json = serde_json::to_string(&rows).unwrap_or_else(|_| "[]".into());+ } let pin_count = serde_json::from_str::<serde_json::Value>(&pins_json).ok() .and_then(|v| v.as_array().map(|a| a.len())).unwrap_or(0); + // Display name: prefer the .lbr's deviceset/symbol name over the dir name.+ let display_name = lbr_text.as_deref()+ .and_then(|l| ["deviceset", "symbol", "package"].iter().find_map(|t| {+ let n = format!("<{t} ");+ let i = l.find(&n)? + n.len();+ let r = &l[i..];+ let k = r.find("name=\"")? + 6;+ let j = r[k..].find('"')? + k;+ Some(r[k..j].to_string())+ }))+ .filter(|s| !s.is_empty())+ .unwrap_or_else(|| mpn.clone()); let html = include_str!("embed.html")- .replace("__NAME__", &esc_html(&mpn))+ .replace("__NAME__", &esc_html(&display_name)) .replace("__SYM__", &sym_svg) .replace("__FP__", &fp_svg) .replace("__THREED__", &threed) .replace("__PINS__", &pins_json) .replace("__PADN__", &pad_count.to_string()) .replace("__PINN__", &pin_count.to_string());- let out = out.unwrap_or_else(|| dir.join(format!("{mpn}-lbr-embed.html")));- fs::write(&out, &html).map_err(|e| format!("writing {}: {e}", out.display()))?;- println!("OK: 3-column interactive embed ({pin_count} pins) → {}", out.display());- Ok(())+ Ok((html, pin_count)) }
src/provenance.rsadded+73@@ -0,0 +1,73 @@+//! Canonical source-provenance LABELING — the single rule for how every Adom+//! EDA app names where a symbol / footprint / 3D / datasheet actually came from.+//!+//! THE RULE (John, 2026-06-23): label by the DISCOVERY SOURCE — where the file+//! was actually downloaded — NOT by content_origin (who authored the model).+//!+//! - ONLY the maker's OWN website counts as "the manufacturer", and then we+//! NAME the site: "Texas Instruments (ti.com)", "Analog Devices (analog.com)".+//! - The moment the manufacturer hands you off to a 3rd-party library —+//! UltraLibrarian, SnapMagic/SnapEDA, Component Search Engine — that+//! AGGREGATOR is the source. NEVER label it "Manufacturer".+//! - A fuzzy bare "Manufacturer" is misleading and banned.+//!+//! This module is mirrored (same mapping) in adom-symbol, adom-footprint and+//! adom-lbr so the label is identical everywhere. See the `chip-provenance-labeling` skill.++/// The maker's canonical website domain, for "Texas Instruments (ti.com)" labels.+/// Matched case-insensitively on a substring of the manufacturer name.+pub fn mfr_domain(manufacturer: &str) -> Option<&'static str> {+ let m = manufacturer.to_lowercase();+ let table = [+ ("texas instr", "ti.com"), ("analog dev", "analog.com"),+ ("stmicro", "st.com"), ("nxp", "nxp.com"), ("microchip", "microchip.com"),+ ("infineon", "infineon.com"), ("on semi", "onsemi.com"), ("onsemi", "onsemi.com"),+ ("nordic", "nordicsemi.com"), ("espressif", "espressif.com"),+ ("renesas", "renesas.com"), ("diodes inc", "diodes.com"), ("vishay", "vishay.com"),+ ("bosch", "bosch-sensortec.com"), ("maxim", "analog.com"), ("dialog", "renesas.com"),+ ("rohm", "rohm.com"), ("toshiba", "toshiba.com"), ("skyworks", "skyworksinc.com"),+ ("qorvo", "qorvo.com"), ("micron", "micron.com"), ("cypress", "infineon.com"),+ ("silicon lab", "silabs.com"), ("silabs", "silabs.com"), ("lattice", "latticesemi.com"),+ ("winbond", "winbond.com"), ("ti ", "ti.com"),+ ];+ table.iter().find(|(k, _)| m.contains(k)).map(|(_, v)| *v)+}++/// The human label for a source, given the discovery-source slug + the+/// manufacturer name. THIS is the function every app calls.+pub fn source_label(discovery: &str, manufacturer: &str) -> String {+ match discovery.to_lowercase().as_str() {+ // Manufacturer-DIRECT: only when downloaded from the maker's own site.+ "manufacturer" | "mfr" | "vendor" => {+ if let Some(dom) = mfr_domain(manufacturer) {+ if manufacturer.is_empty() { format!("Manufacturer · {dom}") }+ else { format!("{manufacturer} ({dom})") }+ } else if !manufacturer.is_empty() {+ format!("{manufacturer} (direct)")+ } else {+ "Manufacturer (direct site)".to_string()+ }+ }+ // 3rd-party aggregators — NEVER "the manufacturer".+ "cse" | "componentsearch" | "cse_ul" | "samacsys" => "Component Search Engine".into(),+ "snapeda" | "snapmagic" => "SnapEDA".into(),+ "ultralibrarian" | "ul" => "Ultra Librarian".into(),+ "mouser" => "Mouser".into(),+ "digikey" => "DigiKey".into(),+ "arrow" => "Arrow".into(),+ "lcsc" => "LCSC".into(),+ "kicad_community" => "KiCad community".into(),+ other => other.to_string(),+ }+}++/// A short note for the "the manufacturer linked you to it, but it's a 3rd+/// party" middle-ground — appended in tooltips so the user knows the maker+/// pointed here even though the file came off an aggregator.+pub fn linked_from_mfr_note(manufacturer: &str) -> String {+ if manufacturer.is_empty() {+ "the manufacturer's page linked to this 3rd-party library".to_string()+ } else {+ format!("{manufacturer}'s own page links here, but the file is hosted by this 3rd-party library — not downloaded from {manufacturer} directly")+ }+}
src/server.rs+15−1@@ -28,6 +28,8 @@ pub struct AppState { } static STATE: Mutex<Option<AppState>> = Mutex::new(None);+/// Filled 3-column (symbol | footprint | 3D) HTML, served at `/` when present.+static EMBED_HTML: Mutex<Option<String>> = Mutex::new(None); static EVAL_PENDING: Mutex<VecDeque<(u64, String)>> = Mutex::new(VecDeque::new()); static EVAL_RESULTS: Mutex<Option<HashMap<u64, Value>>> = Mutex::new(None); static CONSOLE: Mutex<Option<Vec<String>>> = Mutex::new(None);@@ -40,6 +42,11 @@ pub fn set_state(s: AppState) { *STATE.lock().unwrap() = Some(s); } +/// Install the 3-column viewer HTML so `GET /` shows symbol|footprint|3D live.+pub fn set_embed(html: String) {+ *EMBED_HTML.lock().unwrap() = Some(html);+}+ /// Parse + lint an `.lbr` (EAGLE XML) into the displayable app state. pub fn analyze_lbr(content: &str) -> AppState { let lint = crate::lint_lbr(content);@@ -153,7 +160,14 @@ fn hd_post(body: &str) { fn route(method: &Method, url: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> { match (method, url) {- (Method::Get, "/") | (Method::Get, "/index.html") => html(APP_HTML),+ (Method::Get, "/") | (Method::Get, "/index.html") => {+ // 3-column viewer when render assets are loaded; lint card otherwise.+ match &*EMBED_HTML.lock().unwrap() {+ Some(h) => html(h),+ None => html(APP_HTML),+ }+ }+ (Method::Get, "/lint") => html(APP_HTML), (Method::Get, "/favicon.svg") => svg(FAVICON), (Method::Get, "/state") => json_resp(&state_json()), (Method::Post, "/load") => {
uninstall.sh+5−1@@ -1,3 +1,7 @@ #!/bin/bash+# uninstall.sh for adom-lbr (app). set -euo pipefail-rm -f "${HOME}/.local/bin/adom-lbr"; echo "Uninstalled adom-lbr"++rm -f "${HOME}/.local/bin/adom-lbr"+echo "Uninstalled adom-lbr"+