#!/usr/bin/env node // kicad-query.mjs — query KiCad files for Claude-readable summaries // Usage: node kicad-query.mjs [args...] import { readFileSync } from 'fs'; import { extname } from 'path'; // ── S-expression tokenizer ────────────────────────────────────────────────── function tokenize(text) { const tokens = []; let i = 0; const n = text.length; while (i < n) { const ch = text[i]; // whitespace if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n') { i++; continue; } // line comment if (ch === ';') { while (i < n && text[i] !== '\n') i++; continue; } if (ch === '(') { tokens.push('('); i++; continue; } if (ch === ')') { tokens.push(')'); i++; continue; } // quoted string if (ch === '"') { let s = ''; i++; // skip opening quote while (i < n && text[i] !== '"') { if (text[i] === '\\') { i++; const esc = text[i] ?? ''; s += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc; } else { s += text[i]; } i++; } i++; // skip closing quote tokens.push({ s }); continue; } // atom (number, identifier, etc.) let atom = ''; while (i < n && text[i] !== '(' && text[i] !== ')' && text[i] !== '"' && text[i] !== ' ' && text[i] !== '\t' && text[i] !== '\r' && text[i] !== '\n') { atom += text[i++]; } if (atom) tokens.push(atom); } return tokens; } // ── S-expression parser ───────────────────────────────────────────────────── // Returns { tag: string, items: Array } for lists, // or a plain string for atoms/quoted strings. function parse_sexp(text) { const tokens = tokenize(text); let pos = 0; function next() { return tokens[pos++]; } function peek() { return tokens[pos]; } function parse_expr() { const tok = next(); if (tok === '(') { // first item is the tag const tag_tok = next(); const tag = typeof tag_tok === 'string' ? tag_tok : (tag_tok?.s ?? ''); const items = []; while (pos < tokens.length && peek() !== ')') { items.push(parse_expr()); } next(); // consume ')' return { tag, items }; } // atom or quoted string return typeof tok === 'string' ? tok : (tok?.s ?? ''); } return parse_expr(); } // ── Tree helpers ──────────────────────────────────────────────────────────── function find_child(node, tag) { if (!node?.items) return null; return node.items.find(n => n?.tag === tag) ?? null; } function find_children(node, tag) { if (!node?.items) return []; return node.items.filter(n => n?.tag === tag); } // Get value of first atom/string child of a node function first_str(node) { if (!node?.items) return null; for (const item of node.items) { if (typeof item === 'string') return item; } return null; } // Get a (property "Key" "Value") property from a symbol node function get_prop(node, key) { for (const item of (node?.items ?? [])) { if (item?.tag === 'property') { const [k, v] = item.items ?? []; if (k === key) return typeof v === 'string' ? v : (v?.s ?? null); } } return null; } // Collect all nodes recursively matching tag function collect(node, tag, acc = []) { if (!node?.items) return acc; if (node.tag === tag) acc.push(node); for (const item of node.items) { if (item?.items) collect(item, tag, acc); } return acc; } // ── Output helpers ────────────────────────────────────────────────────────── function pad_table(rows, sep = ' ') { if (rows.length === 0) return; const widths = rows[0].map((_, ci) => Math.max(...rows.map(r => String(r[ci] ?? '').length)) ); for (const row of rows) { console.log(row.map((cell, ci) => ci === row.length - 1 ? cell : String(cell ?? '').padEnd(widths[ci]) ).join(sep)); } } function glob_match(pattern, str) { if (!pattern) return true; const re = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i'); return re.test(str); } // Parse "key=value" filter args function parse_filters(args) { const filters = {}; for (const arg of args) { const m = arg.match(/^(\w+)=(.*)$/); if (m) filters[m[1].toLowerCase()] = m[2]; } return filters; } // ── Schematic: data functions ──────────────────────────────────────────────── // Placed symbols (not lib_symbols definitions, not power-only, not unit>1 duplicates) function get_placed_symbols(root) { const lib_syms = find_child(root, 'lib_symbols'); const lib_sym_set = new Set(lib_syms?.items?.map(n => n?.tag === 'symbol' ? first_str(n) : null).filter(Boolean)); // Top-level symbols that are not inside lib_symbols const placed = []; for (const item of (root?.items ?? [])) { if (item?.tag !== 'symbol') continue; // lib_id distinguishes placed symbols from sub-unit definitions const lib_id_node = find_child(item, 'lib_id'); if (!lib_id_node) continue; placed.push(item); } // Filter to unit 1 only (avoid duplicates for multi-unit ICs) return placed.filter(s => { const unit = find_child(s, 'unit'); return !unit || first_str(unit) === '1'; }); } function sch_summary(root) { const all_placed = get_placed_symbols(root); const real = all_placed.filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:')); const power_syms = all_placed.filter(s => (first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:')); const libs = {}; for (const s of real) { const lib = (first_str(find_child(s, 'lib_id')) ?? 'unknown').split(':')[0]; libs[lib] = (libs[lib] ?? 0) + 1; } return { components : real.length, power_symbols : power_syms.length, net_labels : collect(root, 'net_label').length, global_labels : collect(root, 'global_label').length, wires : collect(root, 'wire').length, libraries : Object.fromEntries(Object.entries(libs).sort()), }; } function sch_components(root, filters = {}) { const placed = get_placed_symbols(root); let comps = placed .filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:')) .map(s => ({ ref : get_prop(s, 'Reference') ?? '?', value : get_prop(s, 'Value') ?? '?', footprint : get_prop(s, 'Footprint') ?? '', lib_id : first_str(find_child(s, 'lib_id')) ?? '', datasheet : get_prop(s, 'Datasheet') ?? '', dnp : first_str(find_child(s, 'dnp')) === 'yes', })); if (filters.ref) comps = comps.filter(c => glob_match(filters.ref, c.ref)); if (filters.value) comps = comps.filter(c => glob_match(filters.value, c.value)); if (filters.lib) comps = comps.filter(c => glob_match(filters.lib, c.lib_id.split(':')[0])); if (filters.footprint) comps = comps.filter(c => glob_match(filters.footprint, c.footprint)); comps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true })); return { components: comps }; } function sch_component(root, ref) { const placed = get_placed_symbols(root); const all_units = (root?.items ?? []).filter(item => item?.tag === 'symbol' && find_child(item, 'lib_id') && get_prop(item, 'Reference') === ref ); const s = placed.find(s => get_prop(s, 'Reference') === ref); if (!s && all_units.length === 0) return null; const target = s ?? all_units[0]; const at = find_child(target, 'at'); const props = {}; for (const unit of all_units) { for (const item of (unit?.items ?? [])) { if (item?.tag === 'property') { const k = item.items?.[0], v = item.items?.[1]; if (typeof k === 'string' && typeof v === 'string' && !(k in props)) props[k] = v; } } } const pins = all_units.flatMap(u => find_children(u, 'pin')).map(pin => { const net = find_child(pin, 'net'); const net_name = net ? (first_str(find_child(net, 'name')) ?? first_str(net)) : null; return { num: first_str(pin), net: net_name }; }); return { ref, lib_id : first_str(find_child(target, 'lib_id')) ?? '', position: { x: Number(at?.items?.[0] ?? 0), y: Number(at?.items?.[1] ?? 0) }, properties: props, pins, }; } function sch_nets(root) { const net_labels = collect(root, 'net_label'); const global_labels = collect(root, 'global_label'); const power_syms = (root?.items ?? []).filter(item => item?.tag === 'symbol' && (first_str(find_child(item, 'lib_id')) ?? '').startsWith('power:') ); const locals = new Set(net_labels.map(n => first_str(n)).filter(Boolean)); const globals = new Set(global_labels.map(n => first_str(n)).filter(Boolean)); const power = new Set(power_syms.map(s => get_prop(s, 'Value')).filter(Boolean)); const all = new Set([...locals, ...globals, ...power]); return { nets: [...all].sort().map(name => ({ name, global: globals.has(name), power : power.has(name), })), }; } // ── PCB: data functions ────────────────────────────────────────────────────── function pcb_summary(root) { const real_nets = find_children(root, 'net').filter(n => first_str(n) !== '0'); return { nets : real_nets.length, footprints: collect(root, 'footprint').length, segments : collect(root, 'segment').length, vias : collect(root, 'via').length, zones : collect(root, 'zone').length, }; } function pcb_nets(root) { return { nets: find_children(root, 'net') .filter(n => first_str(n) !== '0') .map(n => ({ id: Number(first_str(n)), name: n.items?.[1] ?? '' })) .sort((a, b) => a.id - b.id), }; } function pcb_net(root, name) { const net_node = find_children(root, 'net').find(n => n.items?.[1] === name); if (!net_node) return null; const net_id = first_str(net_node); const connected_pads = []; for (const fp of collect(root, 'footprint')) { const ref = get_prop(fp, 'Reference') ?? '?'; for (const pad of find_children(fp, 'pad')) { const pad_net = find_child(pad, 'net'); if (pad_net && first_str(pad_net) === net_id) { connected_pads.push({ ref, pad: pad.items?.[0] ?? '?' }); } } } connected_pads.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true })); return { name, id: Number(net_id), connected_pads }; } function pcb_footprints(root, filters = {}) { let fps = collect(root, 'footprint').map(fp => ({ ref : get_prop(fp, 'Reference') ?? '?', value: get_prop(fp, 'Value') ?? '?', lib : first_str(fp) ?? '', layer: first_str(find_child(fp, 'layer')) ?? '', })); if (filters.ref) fps = fps.filter(f => glob_match(filters.ref, f.ref)); if (filters.value) fps = fps.filter(f => glob_match(filters.value, f.value)); fps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true })); return { footprints: fps }; } function pcb_footprint(root, ref) { const fps = collect(root, 'footprint').filter(fp => get_prop(fp, 'Reference') === ref); if (fps.length === 0) return null; const fp = fps[0]; const at = find_child(fp, 'at'); const pads = find_children(fp, 'pad').map(p => ({ num : p.items?.[0] ?? '?', type : p.items?.[1] ?? '?', net_name: find_child(p, 'net')?.items?.[1] ?? '', })); return { ref, lib : first_str(fp) ?? '', layer : first_str(find_child(fp, 'layer')) ?? '', position: { x: Number(at?.items?.[0] ?? 0), y: Number(at?.items?.[1] ?? 0) }, value : get_prop(fp, 'Value') ?? '?', pads, }; } // ── PCB geometry helpers ───────────────────────────────────────────────────── const COORD_TOL = 0.005; // mm tolerance for point equality function pt_eq(ax, ay, bx, by) { return Math.abs(ax - bx) <= COORD_TOL && Math.abs(ay - by) <= COORD_TOL; } // Rotate point (px, py) by angle degrees and translate by (ox, oy) function rotate_translate(px, py, angle_deg, ox, oy) { const rad = (angle_deg * Math.PI) / 180; const cos = Math.cos(rad); const sin = Math.sin(rad); return [ox + px * cos - py * sin, oy + px * sin + py * cos]; } // Get absolute pad position given footprint node function pad_abs_pos(fp_node, pad_node) { const fp_at = find_child(fp_node, 'at'); const fx = Number(fp_at?.items?.[0] ?? 0); const fy = Number(fp_at?.items?.[1] ?? 0); const fr = Number(fp_at?.items?.[2] ?? 0); // footprint rotation const pad_at = find_child(pad_node, 'at'); const px = Number(pad_at?.items?.[0] ?? 0); const py = Number(pad_at?.items?.[1] ?? 0); return rotate_translate(px, py, fr, fx, fy); } // Collect all segments keyed by net id function build_segment_index(root) { const segs = collect(root, 'segment').map(seg => { const s = find_child(seg, 'start'); const e = find_child(seg, 'end'); const net = find_child(seg, 'net'); const lay = find_child(seg, 'layer'); const w = find_child(seg, 'width'); return { sx : Number(s?.items?.[0] ?? 0), sy : Number(s?.items?.[1] ?? 0), ex : Number(e?.items?.[0] ?? 0), ey : Number(e?.items?.[1] ?? 0), net : first_str(net) ?? '', layer: first_str(lay) ?? '', width: Number(first_str(w) ?? 0), }; }); const vias = collect(root, 'via').map(v => { const at = find_child(v, 'at'); const net = find_child(v, 'net'); const layers_node = find_child(v, 'layers'); return { x : Number(at?.items?.[0] ?? 0), y : Number(at?.items?.[1] ?? 0), net : first_str(net) ?? '', layers: layers_node?.items?.filter(i => typeof i === 'string') ?? [], }; }); return { segs, vias }; } // BFS from a set of seed {x, y, layer} points through segments and vias on net_id function trace_from(seed_points, net_id, segs, vias) { // frontier: Set of "x,y,layer" strings const visited_points = new Set(); const visited_segs = new Set(); const visited_vias = new Set(); const queue = [...seed_points]; for (const p of queue) { visited_points.add(`${p.x.toFixed(4)},${p.y.toFixed(4)},${p.layer}`); } const result_segs = []; const result_vias = []; while (queue.length) { const { x, y, layer } = queue.shift(); // Find segments starting or ending here on same layer + net for (let i = 0; i < segs.length; i++) { const seg = segs[i]; if (seg.net !== net_id || seg.layer !== layer) continue; if (visited_segs.has(i)) continue; let other_x, other_y; if (pt_eq(seg.sx, seg.sy, x, y)) { other_x = seg.ex; other_y = seg.ey; } else if (pt_eq(seg.ex, seg.ey, x, y)) { other_x = seg.sx; other_y = seg.sy; } else { continue; } visited_segs.add(i); result_segs.push(seg); const key = `${other_x.toFixed(4)},${other_y.toFixed(4)},${layer}`; if (!visited_points.has(key)) { visited_points.add(key); queue.push({ x: other_x, y: other_y, layer }); } } // Find vias at this point that carry the same net for (let i = 0; i < vias.length; i++) { const via = vias[i]; if (via.net !== net_id) continue; if (!pt_eq(via.x, via.y, x, y)) continue; if (visited_vias.has(i)) continue; visited_vias.add(i); result_vias.push(via); // Expand to all layers the via connects for (const vl of via.layers) { const key = `${via.x.toFixed(4)},${via.y.toFixed(4)},${vl}`; if (!visited_points.has(key)) { visited_points.add(key); queue.push({ x: via.x, y: via.y, layer: vl }); } } } } return { segs: result_segs, vias: result_vias }; } function pcb_traces(root, ref, pad_num) { const fps = collect(root, 'footprint').filter(fp => get_prop(fp, 'Reference') === ref); if (fps.length === 0) return { error: `Footprint '${ref}' not found` }; const fp = fps[0]; const pads = find_children(fp, 'pad'); const pad = pad_num ? pads.find(p => p.items?.[0] === String(pad_num)) : pads[0]; if (!pad) return { error : `Pad '${pad_num}' not found on '${ref}'`, available_pads : pads.map(p => p.items?.[0]), }; const [abs_x, abs_y] = pad_abs_pos(fp, pad); const net_node = find_child(pad, 'net'); const net_id = first_str(net_node) ?? ''; const net_name = net_node?.items?.[1] ?? null; const pad_layers = (find_child(pad, 'layers')?.items ?? []) .filter(i => typeof i === 'string' && i.endsWith('.Cu')); if (!pad_layers.length) pad_layers.push('F.Cu'); if (!net_id || net_id === '0') return { ref, pad: pad.items?.[0], position: { x: abs_x, y: abs_y }, connected: false, }; const { segs, vias } = build_segment_index(root); const seeds = pad_layers.map(l => ({ x: abs_x, y: abs_y, layer: l })); const { segs: found_segs, vias: found_vias } = trace_from(seeds, net_id, segs, vias); return { ref, pad : pad.items?.[0], net : { id: Number(net_id), name: net_name }, position : { x: abs_x, y: abs_y }, pad_layers, traces : found_segs.map(s => ({ layer: s.layer, start: { x: s.sx, y: s.sy }, end : { x: s.ex, y: s.ey }, width: s.width, })), vias: found_vias.map(v => ({ position: { x: v.x, y: v.y }, layers: v.layers })), }; } // ── Generic: data functions ────────────────────────────────────────────────── function generic_tags(root) { const counts = {}; function walk(node) { if (!node?.items) return; counts[node.tag] = (counts[node.tag] ?? 0) + 1; for (const item of node.items) walk(item); } walk(root); return { tags: Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([tag, count]) => ({ tag, count })) }; } function generic_raw(root, tag) { return { tag, nodes: collect(root, tag).map(n => ({ tag : n.tag, items: (n.items ?? []).map(item => typeof item === 'string' ? item : item?.items ? `(${item.tag} ...)` : String(item) ), })), }; } function pro_summary(text) { const proj = JSON.parse(text); const meta = proj.metadata ?? {}; const board = proj.board ?? {}; return { filename : meta.filename ?? null, version : meta.version ?? null, min_clearance: board.design_settings?.rules?.min_clearance ?? null, sections : Object.keys(proj), }; } // ── Query dispatcher (returns data) ───────────────────────────────────────── function run_query(ext, query, root_or_text, args) { const filters = parse_filters(args); if (ext === '.kicad_pro') { switch (query) { case 'summary': return pro_summary(root_or_text); default: return { error: `Unknown query '${query}' for .kicad_pro`, available: ['summary'] }; } } const root = root_or_text; // parsed S-expression tree switch (query) { case 'tags': return generic_tags(root); case 'raw' : return generic_raw(root, args[0]); } if (ext === '.kicad_sch') { switch (query) { case 'summary' : return sch_summary(root); case 'components': return sch_components(root, filters); case 'component' : return sch_component(root, args[0]) ?? { error: `Component '${args[0]}' not found` }; case 'nets' : return sch_nets(root); default: return { error: `Unknown query '${query}' for .kicad_sch`, available: ['summary', 'components', 'component', 'nets', 'tags', 'raw'] }; } } if (ext === '.kicad_pcb') { switch (query) { case 'summary' : return pcb_summary(root); case 'nets' : return pcb_nets(root); case 'net' : return pcb_net(root, args[0]) ?? { error: `Net '${args[0]}' not found` }; case 'footprints': return pcb_footprints(root, filters); case 'footprint' : return pcb_footprint(root, args[0]) ?? { error: `Footprint '${args[0]}' not found` }; case 'traces' : return pcb_traces(root, args[0], args[1]); default: return { error: `Unknown query '${query}' for .kicad_pcb`, available: ['summary', 'nets', 'net', 'footprints', 'footprint', 'traces', 'tags', 'raw'] }; } } return { error: `Unsupported file type '${ext}'` }; } // ── Human renderer ─────────────────────────────────────────────────────────── function render_human(ext, query, data) { if (data?.error) { console.error(`Error: ${data.error}`); return; } // generic if (query === 'tags') { console.log('=== All tags (by frequency) ==='); pad_table([['Tag', 'Count'], ['---', '-----'], ...data.tags.map(t => [t.tag, t.count])]); return; } if (query === 'raw') { console.log(`=== Raw nodes: ${data.tag} (${data.nodes.length}) ===`); for (const n of data.nodes) console.log(` (${n.tag} ${n.items.join(' ')})`); return; } // .kicad_pro if (ext === '.kicad_pro') { console.log('=== KiCad Project ==='); if (data.filename) console.log(`File : ${data.filename}`); if (data.version) console.log(`Version : ${data.version}`); if (data.min_clearance) console.log(`Min clearance: ${data.min_clearance}`); console.log(`Sections : ${data.sections.join(', ')}`); return; } // .kicad_sch if (ext === '.kicad_sch') { if (query === 'summary') { console.log('=== Schematic Summary ==='); console.log(`Components : ${data.components}`); console.log(`Power symbols: ${data.power_symbols}`); console.log(`Net labels : ${data.net_labels}`); console.log(`Global labels: ${data.global_labels}`); console.log(`Wires : ${data.wires}`); if (Object.keys(data.libraries).length) { console.log('\nLibraries:'); for (const [lib, count] of Object.entries(data.libraries)) console.log(` ${lib}: ${count}`); } return; } if (query === 'components') { const comps = data.components; if (comps.length === 0) { console.log('No components match.'); return; } const header = ['Reference', 'Value', 'Library:Symbol', 'Footprint']; pad_table([header, header.map(h => '-'.repeat(h.length)), ...comps.map(c => [c.dnp ? `${c.ref}*` : c.ref, c.value, c.lib_id, c.footprint])]); console.log(`\n${comps.length} component(s)${comps.some(c => c.dnp) ? ' (* = DNP)' : ''}`); return; } if (query === 'component') { console.log(`=== Component: ${data.ref} ===`); console.log(`Library/Symbol : ${data.lib_id}`); console.log(`Position : (${data.position.x}, ${data.position.y})`); console.log('\nProperties:'); for (const [k, v] of Object.entries(data.properties)) if (v && v !== '~') console.log(` ${k}: ${v}`); if (data.pins.length) { console.log(`\nPins (${data.pins.length}):`); for (const p of data.pins) console.log(` pin ${p.num}${p.net ? ` → ${p.net}` : ''}`); } return; } if (query === 'nets') { console.log(`=== Net Names (${data.nets.length} total) ===`); for (const n of data.nets) { const tags = [...(n.global ? ['global'] : []), ...(n.power ? ['power'] : [])]; console.log(` ${n.name}${tags.length ? ' [' + tags.join(', ') + ']' : ''}`); } return; } } // .kicad_pcb if (ext === '.kicad_pcb') { if (query === 'summary') { console.log('=== PCB Summary ==='); console.log(`Nets : ${data.nets}`); console.log(`Footprints : ${data.footprints}`); console.log(`Segments : ${data.segments}`); console.log(`Vias : ${data.vias}`); console.log(`Zones : ${data.zones}`); return; } if (query === 'nets') { if (data.nets.length === 0) { console.log('No nets found.'); return; } console.log(`=== PCB Nets (${data.nets.length}) ===`); pad_table([['ID', 'Net Name'], ['--', '--------'], ...data.nets.map(n => [n.id, n.name])]); return; } if (query === 'net') { console.log(`=== Net: ${data.name} (id=${data.id}) ===`); if (data.connected_pads.length) { console.log(`\nConnected pads (${data.connected_pads.length}):`); for (const { ref, pad } of data.connected_pads) console.log(` ${ref} pad ${pad}`); } else { console.log('No pads connected.'); } return; } if (query === 'footprints') { const fps = data.footprints; if (fps.length === 0) { console.log('No footprints match.'); return; } pad_table([['Reference', 'Value', 'Layer', 'Footprint'], ['---------', '-----', '-----', '---------'], ...fps.map(f => [f.ref, f.value, f.layer, f.lib])]); console.log(`\n${fps.length} footprint(s)`); return; } if (query === 'footprint') { console.log(`=== Footprint: ${data.ref} ===`); console.log(`Library : ${data.lib}`); console.log(`Layer : ${data.layer}`); console.log(`Position : (${data.position.x}, ${data.position.y})`); console.log(`Value : ${data.value}`); if (data.pads.length) { console.log(`\nPads (${data.pads.length}):`); pad_table([['Pad', 'Type', 'Net'], ['---', '----', '---'], ...data.pads.map(p => [p.num, p.type, p.net_name])]); } return; } if (query === 'traces') { if (data.connected === false) { console.log(`${data.ref} pad ${data.pad} is unconnected.`); return; } console.log(`=== Traces from ${data.ref} pad ${data.pad} ===`); console.log(`Net : ${data.net.name} (id=${data.net.id})`); console.log(`Position : (${data.position.x.toFixed(3)}, ${data.position.y.toFixed(3)})`); console.log(`Pad layers : ${data.pad_layers.join(', ')}`); if (data.traces.length === 0 && data.vias.length === 0) { console.log('\nNo traces found connected to this pad.'); return; } const by_layer = {}; for (const seg of data.traces) (by_layer[seg.layer] ??= []).push(seg); for (const [layer, segs] of Object.entries(by_layer).sort()) { console.log(`\nLayer ${layer} — ${segs.length} segment(s):`); pad_table([ [' From (x, y)', '', 'To (x, y)', '', 'Width'], [' -----------', '', '---------', '', '-----'], ...segs.map(s => [ ` (${s.start.x.toFixed(3)},`, `${s.start.y.toFixed(3)})`, `→ (${s.end.x.toFixed(3)},`, `${s.end.y.toFixed(3)})`, `${s.width}mm`, ]), ]); } if (data.vias.length) { console.log(`\nVias (${data.vias.length}):`); for (const v of data.vias) console.log(` (${v.position.x.toFixed(3)}, ${v.position.y.toFixed(3)}) ${v.layers.join(' ↔ ')}`); } return; } } } // ── JSON renderer ──────────────────────────────────────────────────────────── function render_json(data) { console.log(JSON.stringify(data, null, 2)); } // ── Entry point ────────────────────────────────────────────────────────────── function usage() { console.log(`Usage: node kicad-query.mjs [--format=json] [args...] Options: --format=json Emit structured JSON (for LLM consumption) Queries for .kicad_sch: summary High-level statistics components [filters] List placed components (filters: ref=U* value=10k lib=Device) component Details for one component nets All net/label names Queries for .kicad_pcb: summary High-level statistics nets List all nets with IDs net Show which pads connect to a net footprints [filters] List footprints (filters: ref=R* value=10k) footprint Details for one footprint traces [pad] Physical trace path from a pad (BFS through segments/vias) Queries for .kicad_pro: summary Project overview Generic queries (any file): tags All S-expression tags and their counts raw Dump all nodes with a given tag`); } function main() { let argv = process.argv.slice(2); let format = 'human'; for (let i = argv.length - 1; i >= 0; i--) { const m = argv[i].match(/^--format=(\w+)$/); if (m) { format = m[1]; argv.splice(i, 1); } } const [file_arg, query_arg, ...rest_args] = argv; if (!file_arg || file_arg === '--help' || file_arg === '-h') { usage(); process.exit(0); } let text; try { text = readFileSync(file_arg, 'utf8'); } catch (e) { console.error(`Error reading file: ${e.message}`); process.exit(1); } const ext = extname(file_arg).toLowerCase(); const query = query_arg ?? 'summary'; // .kicad_pro stays as text (JSON); everything else gets parsed as S-expression let root_or_text; if (ext === '.kicad_pro') { root_or_text = text; } else { try { root_or_text = parse_sexp(text); } catch (e) { console.error(`Parse error: ${e.message}`); process.exit(1); } } const data = run_query(ext, query, root_or_text, rest_args); if (format === 'json') { render_json(data); } else { render_human(ext, query, data); } } main();