app
Adom Project Manager
Public Made by Adomby adom
Adom Project Manager — the central hub for navigating wiki boards, molecules and libraries into Hydrogen: browse your wiki boards/projects (Personal / Organization / Public), view symbol + footprint + 3D and the per-EDA footprint layer stackup, explore the board and schematic in INTERACTIVE cross-probing viewers (click a net, hover a part for stock and price), add components to your KiCad/Altium libraries, and deliver self-contained KiCad projects. Built on adom-lbr. (Formerly Adom Project Manag
← Commit history
board→library→components resolution; drop F.Fab from PCB; GLB with real component models; whole-project schematic render; publish-board skill
4 files changed
+215−10
package.json+16−4@@ -48,8 +48,20 @@ ], "discovery_pitch": "Browse your wiki boards/projects, inspect each component's symbol, footprint, 3D model and per-EDA layer stackup, add parts to your KiCad/Altium libraries, and deliver self-contained KiCad projects — built on adom-lbr.", "sample_prompts": [- { "prompt": "Open the Adom Project Manager", "label": "Open manager" },- { "prompt": "Add this board's components to my KiCad libraries", "label": "Add to library" },- { "prompt": "Deliver this project as a self-contained KiCad project", "label": "Add project" }+ {+ "prompt": "Open the Adom Project Manager",+ "label": "Open manager"+ },+ {+ "prompt": "Add this board's components to my KiCad libraries",+ "label": "Add to library"+ },+ {+ "prompt": "Deliver this project as a self-contained KiCad project",+ "label": "Add project"+ }+ ],+ "confirmed_user_skills": [+ "publish-board" ]-}+}
skills/publish-board/SKILL.mdadded+93@@ -0,0 +1,93 @@+---+name: publish-board+description: Publish or update a KiCad project to the Adom wiki so it renders in the Adom Project Manager — 3D board, hierarchical schematic, PCB — backed by a component library. Triggers include "upload my kicad project to the wiki", "publish my board", "push my kicad project so I can view it in the project manager", "update my board on the wiki".+user-invocable: true+---++# Publish a KiCad board to the wiki++Turn a local KiCad project into a wiki board page that renders in the **Adom+Project Manager** (rotatable 3D board with real components, hierarchical+schematic, KiCad-colored PCB), backed by a component **library**.++## Data model: board → library → components++- **Board page** — `type: component`, `category: board`, tags `["board", …]` (or+ `molecule`). Ships the render files: `.kicad_pro`, `.kicad_pcb`, EVERY+ `.kicad_sch` (root + sub-sheets), and a `<Board>.3dshapes/` of custom 3D+ models. Its single dependency is the library.+- **Library page** — `type: component`, tag `["library"]`, title+ `"<Board Name> Library"`, slug `<board-slug>-library`. `dependencies` = every+ component on the board. Ships the board's `.kicad_sym` + `.pretty`.+- **Component pages** — the existing per-component pages, matched by MPN → slug.++The Manager reads the board's one dependency (the library), then lists the+library's dependencies (the components). `board_deps` expands any dependency that+itself has dependencies, so this resolves automatically.++## Workflow++### 1. Pull the project (from the user's machine via `adom-desktop`)+Ask for / find the `.kicad_pro`. Pull the project folder (or at least):+`.kicad_pro`, `.kicad_pcb`, **every** `.kicad_sch`, `.kicad_sym`, `.pretty/`.+Use `adom-desktop shell_execute` (find the files) + `pull_file`.++### 2. Analyze it+Run the helper on the pulled folder:+```+python3 skills/publish-board/kicad_board_bundle.py <project-dir>+```+It reports the render files, `board_name`, the 3D model refs+(`models_std` vs `models_custom`), and the `components` (ref, value, mpn, slug).++### 3. Bundle the custom 3D models → `<Board>.3dshapes/`+`models_std` (`${KICAD*_3DMODEL_DIR}/…`) resolve on the render service — leave+them. For each `models_custom` (absolute path), pull the `.step`/`.wrl` from the+user's machine into `<Board>.3dshapes/<basename>` (basename must match the PCB+ref — that's how the Manager's GLB render rewrites `${KIPRJMOD}/3d/<basename>`).++### 4. Map components → wiki slugs (MPN → slug)+Slug = MPN lowercased, non-alphanumeric → `-`. **The helper falls back to the+schematic `Value` when no MPN property exists** — verify the slugs actually match+real wiki component pages (`GET /api/v1/pages/<slug>`), because value-named+symbols (`R_4.3K_0402`) are NOT wiki component slugs. Resolve the real MPN per+part (BOM, the symbol's MPN/LCSC field, or ask the user). Warn + list any that+aren't on the wiki — they won't appear in the component list until published.++### 5. Library page — create or update, and VERIFY completeness+- Slug `<board-slug>-library`, title `"<Board Name> Library"`, `type: component`,+ tags `["library"]`, `dependencies` = the owner-qualified component slugs.+- **On every update, ensure every component on the board is present in the+ library's dependencies; add any missing ones.**+- Ship `.kicad_sym` + `.pretty`.+- Publish needs a hero (generate a simple 1200×630 billboard) + a 3D file for the+ component type (reuse any component `.step`): `adom-wiki pkg publish [--org <org>] --skip-lint`,+ then `repo push` the `package.json` + `.kicad_sym` + `.pretty`.++### 6. Board page — create or update+- `type: component`, `category: board`, tags `["board", …]`,+ `dependencies` = `{ "<owner>/<board-slug>-library": "^0.1.0" }`.+- Needs a hero + a 3D file (reuse a component model or a board render).+- `pkg publish` (this updates the wiki dependency GRAPH — `repo push` alone does+ NOT), then `repo push`: `.kicad_pro`, `.kicad_pcb`, all `.kicad_sch`, the+ `<Board>.3dshapes/`. Set visibility open.++### 7. Verify+Open the Manager; confirm the board appears (Personal/Organization by owner) and+its 3D + schematic + PCB render, and the component list matches the board.++## Gotchas (learned the hard way)++- **repo push ref**: the component namespace is a single shared namespace. Use the+ **bare slug** for `repo push <slug>`; `pkg publish` needs `--org <org>` if the+ slug is owned by an org (a `409 already exists … owned by <org>` tells you which).+- **Dependency graph vs file**: the Manager reads the `package.json` FILE (so+ `repo push` is enough for it), but the wiki's dependency-graph UI comes from+ `pkg publish` — re-publish (bump the version) to update the graph.+- **Hierarchical schematics**: upload the `.kicad_pro` + EVERY `.kicad_sch`. The+ Manager renders the whole project so the root's sheet-symbol boxes resolve; a+ single sheet alone renders wrong.+- **3D models**: only ABSOLUTE-path `(model …)` refs need bundling into+ `.3dshapes/`; `${KICAD*_3DMODEL_DIR}/…` are already on the render service.+- **Every page needs a hero**; components additionally need a 3D file — even the+ library. Generate/reuse one to pass the (hard-enforced) publish lint.
skills/publish-board/kicad_board_bundle.pyadded+75@@ -0,0 +1,75 @@+#!/usr/bin/env python3+"""Analyze a KiCad project for publishing to the wiki (see publish-board SKILL).++Given a project directory (already pulled from the user's machine), report the+render files, the 3D-model refs (standard vs custom-absolute — the custom ones+must be bundled into <Board>.3dshapes/), and the component list with MPN → slug+so the library's dependencies can be filled.++usage: kicad_board_bundle.py <project-dir>+prints JSON: { pro, pcb, schematics[], sym, pretty[], board_name,+ models_std[], models_custom[], components[{ref,value,mpn,slug}] }+"""+import sys, os, re, json, glob+++def slugify(s):+ return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")+++def main(d):+ g = lambda pat: sorted(glob.glob(os.path.join(d, "**", pat), recursive=True))+ pro = (g("*.kicad_pro") or [None])[0]+ pcb = (g("*.kicad_pcb") or [None])[0]+ schs = g("*.kicad_sch")+ sym = (g("*.kicad_sym") or [None])[0]+ pretty = sorted(p for p in g("*.pretty") if os.path.isdir(p))+ board_name = os.path.splitext(os.path.basename(pro or pcb or "board"))[0]++ # 3D model refs from the PCB: ${KICAD*_3DMODEL_DIR}/... resolve on the render+ # service; absolute paths must be bundled into <Board>.3dshapes/ by basename.+ models_std, models_custom = set(), set()+ if pcb:+ txt = open(pcb, encoding="utf-8", errors="ignore").read()+ for m in re.findall(r'\(model\s+"([^"]+)"', txt):+ (models_std if m.startswith("${") else models_custom).add(m)++ # components: each schematic symbol instance's Reference + an MPN-like property+ mpn_keys = ("MPN", "Manufacturer Part Number", "Mfr Part #", "Mfr. Part #",+ "LCSC", "LCSC Part", "LCSC Part #", "Part Number", "Value")+ comps = {}+ for sch in schs:+ t = open(sch, encoding="utf-8", errors="ignore").read()+ for block in re.split(r"\n\s*\(symbol\s", t):+ ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]+)"', block)+ if not ref_m:+ continue+ ref = ref_m.group(1)+ if ref.startswith("#") or ref in comps: # power symbols / dupes+ continue+ mpn = None+ for k in mpn_keys:+ mm = re.search(r'\(property\s+"' + re.escape(k) + r'"\s+"([^"]+)"', block)+ if mm and mm.group(1).strip() and mm.group(1) not in ("~", ""):+ mpn = mm.group(1).strip()+ break+ val_m = re.search(r'\(property\s+"Value"\s+"([^"]+)"', block)+ comps[ref] = {+ "ref": ref,+ "value": val_m.group(1) if val_m else "",+ "mpn": mpn,+ "slug": slugify(mpn) if mpn else None,+ }++ print(json.dumps({+ "pro": pro, "pcb": pcb, "schematics": schs, "sym": sym, "pretty": pretty,+ "board_name": board_name,+ "models_std": sorted(models_std), "models_custom": sorted(models_custom),+ "components": list(comps.values()),+ }, indent=2))+++if __name__ == "__main__":+ if len(sys.argv) < 2:+ sys.exit("usage: kicad_board_bundle.py <project-dir>")+ main(sys.argv[1])
src/main.rs+31−6@@ -267,12 +267,12 @@ fn get_part(slug: &str) -> Value { /// npm-style manifest the wiki dependency graph is built from, so the two agree). /// Values may be `{slug: ver}`, `[slug,…]`, or `[{slug|ref|name}, …]`; owner /// prefixes (`adom/foo`) are stripped to the slug.-fn board_deps(slug: &str) -> Value {+/// A page's package.json dependencies as bare slugs (owner prefix stripped).+fn deps_of(slug: &str) -> Vec<String> { let bare = |k: &str| k.rsplit('/').next().unwrap_or(k).trim().to_string();- let mut deps: Vec<String> = vec![]; if let Some(txt) = wiki_text(slug, "package.json") { if let Ok(v) = serde_json::from_str::<Value>(&txt) {- deps = match v.get("dependencies") {+ return match v.get("dependencies") { Some(Value::Object(m)) => m.keys().map(|k| bare(k)).collect(), Some(Value::Array(a)) => a .iter()@@ -287,14 +287,39 @@ fn board_deps(slug: &str) -> Value { }; } }- let items: Vec<Value> = deps.into_iter().map(|s| json!({"slug": s})).collect();+ vec![]+}++/// Components on a board. Architecture: a board depends on a component LIBRARY,+/// and the library depends on the components. Resolve one hop — any dependency+/// that itself has dependencies is a library and is expanded to its components;+/// a dependency with none is treated as a direct component (backward-compatible+/// with boards that still list components directly).+fn board_deps(slug: &str) -> Value {+ let mut components: Vec<String> = vec![];+ let mut seen = std::collections::HashSet::new();+ for d in deps_of(slug) {+ let sub = deps_of(&d);+ if sub.is_empty() {+ if seen.insert(d.clone()) {+ components.push(d);+ }+ } else {+ for c in sub {+ if seen.insert(c.clone()) {+ components.push(c);+ }+ }+ }+ }+ let items: Vec<Value> = components.into_iter().map(|s| json!({"slug": s})).collect(); // the page's git commit history (the wiki's Activity tab) let log = curl(&format!("{WIKI}/api/v1/pages/{slug}/log")) .and_then(|b| serde_json::from_slice::<Value>(&b).ok()) .and_then(|v| v.get("log").cloned()) .unwrap_or_else(|| json!([])); json!({ "status": "ok", "board": slug, "deps": items, "log": log,- "hints": ["deps come from the board's package.json dependencies (same as the wiki dependency graph)"] })+ "hints": ["board → component library → components (the library's dependencies)"] }) } /// List the file names on a page (robust to any MPN→filename convention — the@@ -899,7 +924,7 @@ fn render_board_media(slug: &str) -> BoardMedia { // draw order = z-order (last on top): bottom/inner copper first (dimmed in // the UI), then F.Cu on top (kept opaque = the solid top layer), then the // silk/edge/fab docs. No F.Mask (its magenta would paint the pads purple).- let pcb_svg = pcb_bytes.as_ref().and_then(|b| kicad_export(slug, "pcb", "svg", "?layers=B.Cu,In2.Cu,In1.Cu,F.Cu,F.Silkscreen,Edge.Cuts,F.Fab", b)).and_then(|b| String::from_utf8(b).ok());+ let pcb_svg = pcb_bytes.as_ref().and_then(|b| kicad_export(slug, "pcb", "svg", "?layers=B.Cu,In2.Cu,In1.Cu,F.Cu,F.Silkscreen,Edge.Cuts", b)).and_then(|b| String::from_utf8(b).ok()); // GLB with the real component 3D models (bundle the custom .3dshapes models) let glb = render_board_glb(slug); (sheets, pcb_svg, glb)