/** * Global Library Table Manager * Registers JLC libraries in KiCad's global sym-lib-table and fp-lib-table * Works cross-platform: macOS, Windows, Linux */ import { readFile, writeFile, mkdir } from 'fs/promises'; import { existsSync } from 'fs'; import { join } from 'path'; import { homedir, platform } from 'os'; import { getAllCategories, getLibraryFilename, getFootprintDirName, get3DModelsDirName, type LibraryCategory, } from './category-router.js'; import { libraryExistsInTable, addLibraryToTable } from './lib-table.js'; // KiCad versions to check (newest first) const KICAD_VERSIONS = ['9.0', '8.0']; // Library prefix - matches category-router.ts const LIBRARY_PREFIX = 'JLC-MCP'; // 3rd party library namespace (subfolder under 3rdparty/) const LIBRARY_NAMESPACE = 'jlc_mcp'; // KiCad environment variable for 3rd party libraries // This resolves to ~/Documents/KiCad/{version}/3rdparty/ on all platforms const KICAD_3RD_PARTY_VAR = '${KICAD9_3RD_PARTY}'; /** * Detect KiCad major version from existing user directories */ function detectKicadVersion(): string { const home = homedir(); const baseDir = join(home, 'Documents', 'KiCad'); for (const version of KICAD_VERSIONS) { if (existsSync(join(baseDir, version))) { return version; } } return '9.0'; // Default } /** * Get KiCad global config directory (platform-specific) * This is where sym-lib-table and fp-lib-table are stored */ function getKicadConfigDir(version: string): string { const home = homedir(); const plat = platform(); if (plat === 'darwin') { return join(home, 'Library', 'Preferences', 'kicad', version); } else if (plat === 'win32') { return join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'kicad', version); } else { // Linux and others return join(home, '.config', 'kicad', version); } } /** * Get KiCad 3rd party library base directory (where libraries are stored) * Platform-specific paths matching where ${KICAD9_3RD_PARTY} resolves: * - macOS/Windows: ~/Documents/KiCad/{version}/3rdparty/ * - Linux: ~/.local/share/kicad/{version}/3rdparty/ */ function get3rdPartyBaseDir(version: string): string { const home = homedir(); const plat = platform(); if (plat === 'linux') { return join(home, '.local', 'share', 'kicad', version, '3rdparty', LIBRARY_NAMESPACE); } // macOS and Windows use Documents/KiCad return join(home, 'Documents', 'KiCad', version, '3rdparty', LIBRARY_NAMESPACE); } /** * Get absolute path to a JLC-MCP symbol library file (for file operations) */ function getSymbolLibPath(category: LibraryCategory, version: string): string { return join(get3rdPartyBaseDir(version), 'symbols', getLibraryFilename(category)); } /** * Get portable URI for a JLC-MCP symbol library (for table entries) * Uses ${KICAD9_3RD_PARTY} variable for cross-system compatibility */ function getSymbolLibUri(category: LibraryCategory): string { return `${KICAD_3RD_PARTY_VAR}/${LIBRARY_NAMESPACE}/symbols/${getLibraryFilename(category)}`; } /** * Get absolute path to JLC-MCP footprint library directory (for file operations) */ function getFootprintLibPath(version: string): string { return join(get3rdPartyBaseDir(version), 'footprints', getFootprintDirName()); } /** * Get portable URI for JLC-MCP footprint library (for table entries) * Uses ${KICAD9_3RD_PARTY} variable for cross-system compatibility */ function getFootprintLibUri(): string { return `${KICAD_3RD_PARTY_VAR}/${LIBRARY_NAMESPACE}/footprints/${getFootprintDirName()}`; } /** * Generate standard KiCad symbol library header */ function generateEmptySymbolLibrary(): string { return `(kicad_symbol_lib \t(version 20241209) \t(generator "jlc-mcp") \t(generator_version "9.0") )\n`; } // Library description for table entries const LIBRARY_DESCRIPTION = 'Autogenerated by JLC-MCP'; /** * Generate new sym-lib-table content with all JLC-MCP libraries * Uses ${KICAD9_3RD_PARTY} variable for portable paths */ function generateSymLibTable(): string { const categories = getAllCategories(); let content = '(sym_lib_table\n (version 7)\n'; for (const category of categories) { const name = `${LIBRARY_PREFIX}-${category}`; const uri = getSymbolLibUri(category); content += ` (lib (name "${name}")(type "KiCad")(uri "${uri}")(options "")(descr "${LIBRARY_DESCRIPTION}"))\n`; } content += ')\n'; return content; } /** * Generate new fp-lib-table content with JLC-MCP footprint library * Uses ${KICAD9_3RD_PARTY} variable for portable paths */ function generateFpLibTable(): string { const name = LIBRARY_PREFIX; const uri = getFootprintLibUri(); return `(fp_lib_table (version 7) (lib (name "${name}")(type "KiCad")(uri "${uri}")(options "")(descr "${LIBRARY_DESCRIPTION}")) ) `; } interface TableUpdateResult { path: string; created: boolean; modified: boolean; entriesAdded: number; } /** * Ensure sym-lib-table has all JLC-MCP symbol libraries registered */ async function ensureGlobalSymLibTable(version: string): Promise { const configDir = getKicadConfigDir(version); const tablePath = join(configDir, 'sym-lib-table'); const categories = getAllCategories(); // Ensure config directory exists await mkdir(configDir, { recursive: true }); if (!existsSync(tablePath)) { // Create new table with all libraries const content = generateSymLibTable(); await writeFile(tablePath, content, 'utf-8'); return { path: tablePath, created: true, modified: false, entriesAdded: categories.length, }; } // Read existing table and add missing libraries let content = await readFile(tablePath, 'utf-8'); let entriesAdded = 0; for (const category of categories) { const name = `${LIBRARY_PREFIX}-${category}`; if (!libraryExistsInTable(content, name)) { const uri = getSymbolLibUri(category); content = addLibraryToTable(content, name, uri, 'sym', LIBRARY_DESCRIPTION); // Validate entry was added correctly if (!libraryExistsInTable(content, name)) { throw new Error(`Failed to add symbol library ${name} to table`); } entriesAdded++; } } if (entriesAdded > 0) { await writeFile(tablePath, content, 'utf-8'); return { path: tablePath, created: false, modified: true, entriesAdded, }; } return { path: tablePath, created: false, modified: false, entriesAdded: 0, }; } /** * Ensure fp-lib-table has JLC-MCP footprint library registered */ async function ensureGlobalFpLibTable(version: string): Promise { const configDir = getKicadConfigDir(version); const tablePath = join(configDir, 'fp-lib-table'); const name = LIBRARY_PREFIX; // Ensure config directory exists await mkdir(configDir, { recursive: true }); if (!existsSync(tablePath)) { // Create new table const content = generateFpLibTable(); await writeFile(tablePath, content, 'utf-8'); return { path: tablePath, created: true, modified: false, entriesAdded: 1, }; } // Read existing table and add if missing let content = await readFile(tablePath, 'utf-8'); if (!libraryExistsInTable(content, name)) { const uri = getFootprintLibUri(); content = addLibraryToTable(content, name, uri, 'fp', LIBRARY_DESCRIPTION); // Validate entry was added correctly if (!libraryExistsInTable(content, name)) { throw new Error(`Failed to add footprint library ${name} to table`); } await writeFile(tablePath, content, 'utf-8'); return { path: tablePath, created: false, modified: true, entriesAdded: 1, }; } return { path: tablePath, created: false, modified: false, entriesAdded: 0, }; } /** * Ensure library directories and empty stub files exist * Creates structure under ~/Documents/KiCad/{version}/3rdparty/jlc_mcp/ */ async function ensureLibraryStubs(version: string): Promise<{ symbolsCreated: string[]; directoriesCreated: string[]; }> { const baseDir = get3rdPartyBaseDir(version); const symbolsDir = join(baseDir, 'symbols'); const footprintsDir = join(baseDir, 'footprints', getFootprintDirName()); const models3dDir = join(baseDir, '3dmodels', get3DModelsDirName()); const directoriesCreated: string[] = []; const symbolsCreated: string[] = []; // Create directories for (const dir of [symbolsDir, footprintsDir, models3dDir]) { if (!existsSync(dir)) { await mkdir(dir, { recursive: true }); directoriesCreated.push(dir); } } // Create empty symbol library files const categories = getAllCategories(); const emptyContent = generateEmptySymbolLibrary(); for (const category of categories) { const filePath = getSymbolLibPath(category, version); if (!existsSync(filePath)) { await writeFile(filePath, emptyContent, 'utf-8'); symbolsCreated.push(filePath); } } return { symbolsCreated, directoriesCreated }; } export interface GlobalRegistrationResult { success: boolean; version: string; symLibTable: TableUpdateResult; fpLibTable: TableUpdateResult; libraryStubs: { symbolsCreated: string[]; directoriesCreated: string[]; }; errors: string[]; } /** * Main entry point: Ensure JLC libraries are registered in KiCad global tables * Call this on MCP server startup */ // EasyEDA library naming const EASYEDA_LIBRARY_NAME = 'EasyEDA'; const EASYEDA_SYMBOL_LIBRARY_NAME = 'EasyEDA.kicad_sym'; const EASYEDA_FOOTPRINT_LIBRARY_NAME = 'EasyEDA.pretty'; const EASYEDA_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library'; /** * Get portable URI for EasyEDA symbol library (for global table entries) */ function getEasyEDASymbolLibUri(): string { return `${KICAD_3RD_PARTY_VAR}/${LIBRARY_NAMESPACE}/symbols/${EASYEDA_SYMBOL_LIBRARY_NAME}`; } /** * Get portable URI for EasyEDA footprint library (for global table entries) */ function getEasyEDAFootprintLibUri(): string { return `${KICAD_3RD_PARTY_VAR}/${LIBRARY_NAMESPACE}/footprints/${EASYEDA_FOOTPRINT_LIBRARY_NAME}`; } /** * Ensure EasyEDA library is registered in global sym-lib-table and fp-lib-table */ export async function ensureGlobalEasyEDALibrary(): Promise { const version = detectKicadVersion(); const configDir = getKicadConfigDir(version); // Ensure config directory exists await mkdir(configDir, { recursive: true }); // Update sym-lib-table const symTablePath = join(configDir, 'sym-lib-table'); if (existsSync(symTablePath)) { let content = await readFile(symTablePath, 'utf-8'); if (!libraryExistsInTable(content, EASYEDA_LIBRARY_NAME)) { const uri = getEasyEDASymbolLibUri(); content = addLibraryToTable(content, EASYEDA_LIBRARY_NAME, uri, 'sym', EASYEDA_LIBRARY_DESCRIPTION); await writeFile(symTablePath, content, 'utf-8'); } } else { // Create new table with EasyEDA library const uri = getEasyEDASymbolLibUri(); const content = `(sym_lib_table (version 7) (lib (name "${EASYEDA_LIBRARY_NAME}")(type "KiCad")(uri "${uri}")(options "")(descr "${EASYEDA_LIBRARY_DESCRIPTION}")) ) `; await writeFile(symTablePath, content, 'utf-8'); } // Update fp-lib-table const fpTablePath = join(configDir, 'fp-lib-table'); if (existsSync(fpTablePath)) { let content = await readFile(fpTablePath, 'utf-8'); if (!libraryExistsInTable(content, EASYEDA_LIBRARY_NAME)) { const uri = getEasyEDAFootprintLibUri(); content = addLibraryToTable(content, EASYEDA_LIBRARY_NAME, uri, 'fp', EASYEDA_LIBRARY_DESCRIPTION); await writeFile(fpTablePath, content, 'utf-8'); } } else { // Create new table with EasyEDA library const uri = getEasyEDAFootprintLibUri(); const content = `(fp_lib_table (version 7) (lib (name "${EASYEDA_LIBRARY_NAME}")(type "KiCad")(uri "${uri}")(options "")(descr "${EASYEDA_LIBRARY_DESCRIPTION}")) ) `; await writeFile(fpTablePath, content, 'utf-8'); } } export async function ensureGlobalLibraryTables(): Promise { const errors: string[] = []; const version = detectKicadVersion(); let symLibTable: TableUpdateResult = { path: '', created: false, modified: false, entriesAdded: 0, }; let fpLibTable: TableUpdateResult = { path: '', created: false, modified: false, entriesAdded: 0, }; let libraryStubs = { symbolsCreated: [] as string[], directoriesCreated: [] as string[], }; // Step 1: Create library stubs (directories and empty files) try { libraryStubs = await ensureLibraryStubs(version); } catch (error) { const msg = `Failed to create library stubs: ${error instanceof Error ? error.message : String(error)}`; errors.push(msg); // Continue anyway - tables might still work if directories exist } // Step 2: Update global sym-lib-table try { symLibTable = await ensureGlobalSymLibTable(version); } catch (error) { const msg = `Failed to update sym-lib-table: ${error instanceof Error ? error.message : String(error)}`; errors.push(msg); } // Step 3: Update global fp-lib-table try { fpLibTable = await ensureGlobalFpLibTable(version); } catch (error) { const msg = `Failed to update fp-lib-table: ${error instanceof Error ? error.message : String(error)}`; errors.push(msg); } // Success if at least the tables were updated (even if stubs failed) const success = symLibTable.path !== '' && fpLibTable.path !== '' && errors.length === 0; return { success, version, symLibTable, fpLibTable, libraryStubs, errors, }; }