app
KiCad Library Manager
Public Made by Adomby adom
Organize your KiCad symbol / footprint / 3D libraries fast — three priority views, inline editing, live previews, and one-click sync.
← Commit history
package.json+2−2@@ -4,9 +4,9 @@ "slug": "kicad-library-manager", "title": "KiCad Library Manager", "brief": "Organize your KiCad symbol / footprint / 3D libraries fast — three priority views, inline editing, live previews, and one-click sync.",- "version": "1.1.0",+ "version": "1.1.1", "visibility": "public",- "changelog": "## 1.1.0\n- Footprint previews: pads render above silkscreen with pad numbers on top\n- 3D model preview (STEP→GLB) for footprints and a dedicated 3D Models view\n- Live JLCPCB stock + price on hover over an LCSC part number\n- Pending changes: deletes flag red, adds green, edits outline the changed field orange; header chip shows new / deleted / changed counts\n- Sync is a repeatable one-shot (resets after) with per-library sync; deleted libraries get unregistered from the lib-tables\n- Tabs renamed to Symbols / Footprints / 3D Models",+ "changelog": "## 1.1.1\n- Sync now prunes dead lib-table entries (paths that no longer exist) from sym-lib-table / fp-lib-table, so KiCad stops erroring on launch after a library is deleted. Backs up each table to .klm.bak. Also exposed as POST /api/prune-tables.\n\n## 1.1.0\n- Footprint previews: pads render above silkscreen with pad numbers on top\n- 3D model preview (STEP→GLB) for footprints and a dedicated 3D Models view\n- Live JLCPCB stock + price on hover over an LCSC part number\n- Pending changes: deletes flag red, adds green, edits outline the changed field orange; header chip shows new / deleted / changed counts\n- Sync is a repeatable one-shot (resets after) with per-library sync; deleted libraries get unregistered from the lib-tables\n- Tabs renamed to Symbols / Footprints / 3D Models", "discovery_triggers": [ "kicad library manager", "organize my kicad libraries",
page.json+2−2@@ -4,7 +4,7 @@ "slug": "kicad-library-manager", "title": "KiCad Library Manager", "brief": "Organize your KiCad symbol / footprint / 3D libraries fast — three priority views, inline editing, live previews, and one-click sync.",- "version": "1.1.0",+ "version": "1.1.1", "description": "A fast workbench for your personal KiCad libraries. Probes your machine via adom-desktop for your sym-lib-table / fp-lib-table, then gives you three views — Symbol priority (inline-editable ref / footprint / datasheet / LCSC PN / description, with footprint autocomplete and live JLCPCB stock+price on hover), Footprint priority (3D model + the symbols using each footprint), and 3D-model priority. Add parts from the Adom wiki or blank, duplicate symbols, flag deletes (red) and adds (green) with an undo/redo tree, preview symbols + footprints + 3D models with kicad-cli rendering, and sync changes back to disk with reversible backups.", "dependencies": {}, "tags": [@@ -62,5 +62,5 @@ "lcsc stock and price", "klm" ],- "changelog": "## 1.1.0\n- Footprint previews: pads render above silkscreen with pad numbers on top\n- 3D model preview (STEP→GLB) for footprints and a dedicated 3D Models view\n- Live JLCPCB stock + price on hover over an LCSC part number\n- Pending changes: deletes flag red, adds green, edits outline the changed field orange; header chip shows new / deleted / changed counts\n- Sync is a repeatable one-shot (resets after) with per-library sync; deleted libraries get unregistered from the lib-tables\n- Tabs renamed to Symbols / Footprints / 3D Models"+ "changelog": "## 1.1.1\n- Sync now prunes dead lib-table entries (paths that no longer exist) from sym-lib-table / fp-lib-table, so KiCad stops erroring on launch after a library is deleted. Backs up each table to .klm.bak. Also exposed as POST /api/prune-tables.\n\n## 1.1.0\n- Footprint previews: pads render above silkscreen with pad numbers on top\n- 3D model preview (STEP→GLB) for footprints and a dedicated 3D Models view\n- Live JLCPCB stock + price on hover over an LCSC part number\n- Pending changes: deletes flag red, adds green, edits outline the changed field orange; header chip shows new / deleted / changed counts\n- Sync is a repeatable one-shot (resets after) with per-library sync; deleted libraries get unregistered from the lib-tables\n- Tabs renamed to Symbols / Footprints / 3D Models" }
server.js+50@@ -748,6 +748,45 @@ async function unregisterLibs(items) { } return out; }+// Remove lib-table entries whose path no longer exists (e.g. a library file the user deleted), so+// KiCad stops erroring on launch. Leaves KiCad stock/project entries (${KICAD*}, ${KIPRJMOD}) alone —+// those resolve via KiCad's own env. Runs on every sync. Backs up each table to .klm.bak.+let DESKTOP_HOME = null;+async function desktopHome() {+ if (DESKTOP_HOME) return DESKTOP_HOME;+ const o = await runDesktopShell('printf %s "$HOME"', 15);+ DESKTOP_HOME = (o || '').trim() || HOME; return DESKTOP_HOME;+}+function expandDesktop(p, home) {+ return (p || '').replace(/^~(?=[\\/]|$)/, home).replace(/\$\{HOME\}/g, home).replace(/\$HOME\b/g, home).replace(/\$\{USERPROFILE\}/g, home);+}+async function pruneDeadLibTableEntries() {+ const tables = (DB.probe && DB.probe.tablesFound) || [];+ const result = { removed: [], tables: [] };+ if (!tables.length) return result;+ const home = await desktopHome();+ for (const tp of tables) {+ const text = await runDesktopShell(`[ -f ${shq(tp)} ] && cat ${shq(tp)} || echo __KLM_NOFILE__`, 30);+ if (!text || text.includes('__KLM_NOFILE__')) continue;+ const checks = []; // concrete-path entries we can verify (skip ${KICAD*}/${KIPRJMOD}/${KISYS*})+ for (const lib of parseLibTable(text)) {+ if (/\$\{KICAD|\$\{KIPRJMOD\}|\$\{KISYS/i.test(lib.uri)) continue;+ const pth = expandDesktop(lib.uri, home);+ if (/\$\{/.test(pth)) continue; // unknown custom var — leave it alone+ checks.push({ name: lib.name, path: pth });+ }+ if (!checks.length) continue;+ const probe = checks.map(c => `[ -e ${shq(c.path)} ] || echo ${shq('MISS::' + c.name)}`).join('; ');+ const out = await runDesktopShell(probe, 40);+ const dead = [...new Set(out.split('\n').map(l => l.trim()).filter(l => l.startsWith('MISS::')).map(l => l.slice(6)))];+ if (!dead.length) continue;+ const pats = dead.map(n => `-e ${shq('(name "' + n + '")')}`).join(' ');+ const cmd = `T=${shq(tp)}; cp -f "$T" "$T.klm.bak"; grep -Fv ${pats} "$T" > "$T.klm.tmp" && mv "$T.klm.tmp" "$T" && echo OK`;+ const res = await runDesktopShell(cmd, 40);+ if (/OK/.test(res)) { for (const n of dead) result.removed.push({ table: baseOf(tp), name: n }); result.tables.push(tp); }+ }+ return result;+} async function restoreTrash() { // move everything in the trash back to its sibling .pretty / model dir (best-effort: by basename mapping). const trashRoot = kiRoot() + '/' + TRASH_DIRNAME;@@ -811,6 +850,9 @@ async function runSync() { for (const lib of DB.libs.footprintLibs) if (lib.pendingDelete) unregItems.push({ nick: lib.nick, table: 'fp' }); const unreg = await unregisterLibs(unregItems); if (unreg.removed.length) log.push(`Unregistered ${unreg.removed.length} library(ies) from the lib-table(s)`);+ // 5b) clean any lib-table entries whose path no longer exists (prevents KiCad launch errors)+ const dead = await pruneDeadLibTableEntries();+ if (dead.removed.length) log.push(`Removed ${dead.removed.length} dead lib-table entry(ies): ${dead.removed.map(d => d.name).join(', ')}`); // 6) prune the model: drop the now-deleted items, clear the pending-add flags pruneAfterSync(); // one-shot: reset to idle so the button is immediately ready to sync again@@ -905,6 +947,9 @@ async function syncOneLibrary(mode, nick) { DB.libs.models3d = DB.libs.models3d.filter(md => !((md.folder || '(root)') === nick && md.pendingDelete)); log.push(`wrote ${n} model(s), removed ${del.length}`); } else throw new Error('bad mode: ' + mode);+ // every push also cleans dead lib-table entries (paths that no longer exist)+ const dead = await pruneDeadLibTableEntries();+ if (dead.removed.length) log.push(`Removed ${dead.removed.length} dead lib-table entry(ies): ${dead.removed.map(d => d.name).join(', ')}`); recomputeUsage(); DB.sync.lastSyncedAt = new Date().toISOString(); DB.sync.log = log; save(); return log;@@ -1259,6 +1304,11 @@ const server = http.createServer(async (req, res) => { return sendJSON(res, 200, { ok: true, opened: true, pushed: !onDisk && !!push }); } catch (e) { return sendJSON(res, 500, { error: e.message }); } }+ // ---- clean dead lib-table entries (paths that no longer exist) on demand+ if (p === '/api/prune-tables' && req.method === 'POST') {+ try { const r = await pruneDeadLibTableEntries(); return sendJSON(res, 200, { ok: true, removed: r.removed }); }+ catch (e) { return sendJSON(res, 500, { error: e.message }); }+ } if (p === '/api/sync-library' && req.method === 'POST') { const { mode, nick } = await readJSON(req); if (!mode || !nick) return sendJSON(res, 400, { error: 'mode and nick required' });