@jlcpcb/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +474 -0
  3. package/package.json +48 -0
  4. package/src/api/easyeda-community.ts +259 -0
  5. package/src/api/easyeda.ts +153 -0
  6. package/src/api/index.ts +7 -0
  7. package/src/api/jlc.ts +185 -0
  8. package/src/constants/design-rules.ts +119 -0
  9. package/src/constants/footprints.ts +68 -0
  10. package/src/constants/index.ts +7 -0
  11. package/src/constants/kicad.ts +147 -0
  12. package/src/converter/category-router.ts +638 -0
  13. package/src/converter/footprint-mapper.ts +236 -0
  14. package/src/converter/footprint.ts +949 -0
  15. package/src/converter/global-lib-table.ts +394 -0
  16. package/src/converter/index.ts +46 -0
  17. package/src/converter/lib-table.ts +181 -0
  18. package/src/converter/svg-arc.ts +179 -0
  19. package/src/converter/symbol-templates.ts +214 -0
  20. package/src/converter/symbol.ts +1682 -0
  21. package/src/converter/value-normalizer.ts +262 -0
  22. package/src/index.ts +25 -0
  23. package/src/parsers/easyeda-shapes.ts +628 -0
  24. package/src/parsers/http-client.ts +96 -0
  25. package/src/parsers/index.ts +38 -0
  26. package/src/parsers/utils.ts +29 -0
  27. package/src/services/component-service.ts +100 -0
  28. package/src/services/fix-service.ts +50 -0
  29. package/src/services/index.ts +9 -0
  30. package/src/services/library-service.ts +696 -0
  31. package/src/types/component.ts +61 -0
  32. package/src/types/easyeda-community.ts +78 -0
  33. package/src/types/easyeda.ts +356 -0
  34. package/src/types/index.ts +12 -0
  35. package/src/types/jlc.ts +84 -0
  36. package/src/types/kicad.ts +136 -0
  37. package/src/types/mcp.ts +77 -0
  38. package/src/types/project.ts +60 -0
  39. package/src/utils/conversion.ts +104 -0
  40. package/src/utils/file-system.ts +143 -0
  41. package/src/utils/index.ts +8 -0
  42. package/src/utils/logger.ts +96 -0
  43. package/src/utils/validation.ts +110 -0
  44. package/tsconfig.json +9 -0
@@ -0,0 +1,696 @@
1
+ /**
2
+ * Library Service
3
+ * High-level API for installing components to KiCad libraries
4
+ */
5
+
6
+ import { existsSync } from 'fs';
7
+ import { readFile, readdir } from 'fs/promises';
8
+ import { homedir, platform } from 'os';
9
+ import { join, basename } from 'path';
10
+
11
+ import type { EasyEDAComponentData, EasyEDACommunityComponent } from '../types/index.js';
12
+ import type { LibraryCategory } from '../converter/category-router.js';
13
+ import { ensureDir, writeText, writeBinary } from '../utils/index.js';
14
+ import { easyedaClient } from '../api/easyeda.js';
15
+ import { easyedaCommunityClient } from '../api/easyeda-community.js';
16
+ import { jlcClient } from '../api/jlc.js';
17
+ import { symbolConverter } from '../converter/symbol.js';
18
+ import { footprintConverter } from '../converter/footprint.js';
19
+ import {
20
+ ensureSymLibTable,
21
+ ensureFpLibTable,
22
+ } from '../converter/lib-table.js';
23
+ import {
24
+ getLibraryCategory,
25
+ getLibraryFilename,
26
+ getFootprintDirName,
27
+ get3DModelsDirName,
28
+ getSymbolReference,
29
+ getFootprintReference,
30
+ getAllCategories,
31
+ } from '../converter/category-router.js';
32
+
33
+ // Library naming
34
+ const FOOTPRINT_LIBRARY_NAME = getFootprintDirName();
35
+ const MODELS_3D_NAME = get3DModelsDirName();
36
+ const LIBRARY_NAMESPACE = 'jlc_mcp';
37
+
38
+ // KiCad versions to check
39
+ const KICAD_VERSIONS = ['9.0', '8.0'];
40
+
41
+ // EasyEDA community library naming
42
+ const EASYEDA_LIBRARY_NAME = 'EasyEDA';
43
+ const EASYEDA_SYMBOL_LIBRARY_NAME = 'EasyEDA.kicad_sym';
44
+ const EASYEDA_FOOTPRINT_LIBRARY_NAME = 'EasyEDA.pretty';
45
+ const EASYEDA_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library';
46
+
47
+ export interface InstallOptions {
48
+ projectPath?: string;
49
+ include3d?: boolean;
50
+ force?: boolean;
51
+ }
52
+
53
+ export interface InstallResult {
54
+ success: boolean;
55
+ id: string;
56
+ source: 'lcsc' | 'easyeda_community';
57
+ storageMode: 'global' | 'project-local';
58
+ category?: string;
59
+ symbolName: string;
60
+ symbolRef: string;
61
+ footprintRef: string;
62
+ footprintType: 'reference' | 'generated';
63
+ datasheet?: string;
64
+ files: {
65
+ symbolLibrary: string;
66
+ footprint?: string;
67
+ model3d?: string;
68
+ };
69
+ symbolAction: 'created' | 'appended' | 'exists' | 'replaced';
70
+ validationData: ValidationData;
71
+ }
72
+
73
+ export interface ValidationData {
74
+ component: {
75
+ name: string;
76
+ description?: string;
77
+ package?: string;
78
+ manufacturer?: string;
79
+ datasheet_url?: string;
80
+ };
81
+ symbol: {
82
+ pin_count: number;
83
+ pins: Array<{ number: string; name: string; electrical_type?: string }>;
84
+ };
85
+ footprint: {
86
+ type: string;
87
+ pad_count: number;
88
+ pads?: Array<{ number: string; shape: string }> | null;
89
+ is_kicad_standard: boolean;
90
+ kicad_ref: string;
91
+ };
92
+ checks: {
93
+ pin_pad_count_match: boolean;
94
+ has_power_pins: boolean;
95
+ has_ground_pins: boolean;
96
+ };
97
+ }
98
+
99
+ export interface InstalledComponent {
100
+ lcscId: string;
101
+ name: string;
102
+ category: LibraryCategory;
103
+ symbolRef: string;
104
+ footprintRef: string;
105
+ library: string;
106
+ has3dModel: boolean;
107
+ }
108
+
109
+ export interface LibraryStatus {
110
+ installed: boolean;
111
+ linked: boolean;
112
+ version: string;
113
+ componentCount: number;
114
+ paths: {
115
+ symbolsDir: string;
116
+ footprintsDir: string;
117
+ models3dDir: string;
118
+ symLibTable: string;
119
+ fpLibTable: string;
120
+ };
121
+ }
122
+
123
+ export interface ListOptions {
124
+ category?: LibraryCategory;
125
+ projectPath?: string;
126
+ }
127
+
128
+ export interface UpdateOptions {
129
+ category?: LibraryCategory;
130
+ projectPath?: string;
131
+ dryRun?: boolean;
132
+ }
133
+
134
+ export interface UpdateResult {
135
+ updated: number;
136
+ failed: number;
137
+ skipped: number;
138
+ components: Array<{ id: string; status: 'updated' | 'failed' | 'skipped'; error?: string }>;
139
+ }
140
+
141
+ export interface LibraryService {
142
+ install(id: string, options?: InstallOptions): Promise<InstallResult>;
143
+ listInstalled(options?: ListOptions): Promise<InstalledComponent[]>;
144
+ update(options?: UpdateOptions): Promise<UpdateResult>;
145
+ ensureGlobalTables(): Promise<void>;
146
+ getStatus(): Promise<LibraryStatus>;
147
+ }
148
+
149
+ interface LibraryPaths {
150
+ base: string;
151
+ symbolsDir: string;
152
+ footprintsDir: string;
153
+ models3dDir: string;
154
+ footprintDir: string;
155
+ models3dFullDir: string;
156
+ }
157
+
158
+ function detectKicadVersion(): string {
159
+ const home = homedir();
160
+ const baseDir = join(home, 'Documents', 'KiCad');
161
+
162
+ for (const version of KICAD_VERSIONS) {
163
+ if (existsSync(join(baseDir, version))) {
164
+ return version;
165
+ }
166
+ }
167
+ return '9.0';
168
+ }
169
+
170
+ function getGlobalLibraryPaths(): LibraryPaths {
171
+ const home = homedir();
172
+ const version = detectKicadVersion();
173
+ const plat = platform();
174
+
175
+ let base: string;
176
+ if (plat === 'linux') {
177
+ base = join(home, '.local', 'share', 'kicad', version, '3rdparty', LIBRARY_NAMESPACE);
178
+ } else {
179
+ base = join(home, 'Documents', 'KiCad', version, '3rdparty', LIBRARY_NAMESPACE);
180
+ }
181
+
182
+ return {
183
+ base,
184
+ symbolsDir: join(base, 'symbols'),
185
+ footprintsDir: join(base, 'footprints'),
186
+ models3dDir: join(base, '3dmodels'),
187
+ footprintDir: join(base, 'footprints', FOOTPRINT_LIBRARY_NAME),
188
+ models3dFullDir: join(base, '3dmodels', MODELS_3D_NAME),
189
+ };
190
+ }
191
+
192
+ function getProjectLibraryPaths(projectPath: string): LibraryPaths {
193
+ const librariesDir = join(projectPath, 'libraries');
194
+
195
+ return {
196
+ base: librariesDir,
197
+ symbolsDir: join(librariesDir, 'symbols'),
198
+ footprintsDir: join(librariesDir, 'footprints'),
199
+ models3dDir: join(librariesDir, '3dmodels'),
200
+ footprintDir: join(librariesDir, 'footprints', FOOTPRINT_LIBRARY_NAME),
201
+ models3dFullDir: join(librariesDir, '3dmodels', MODELS_3D_NAME),
202
+ };
203
+ }
204
+
205
+ function isLcscId(id: string): boolean {
206
+ return /^C\d+$/.test(id);
207
+ }
208
+
209
+ function getKicadConfigDir(version: string): string {
210
+ const home = homedir();
211
+ const plat = platform();
212
+
213
+ if (plat === 'darwin') {
214
+ return join(home, 'Library', 'Preferences', 'kicad', version);
215
+ } else if (plat === 'win32') {
216
+ return join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'kicad', version);
217
+ } else {
218
+ return join(home, '.config', 'kicad', version);
219
+ }
220
+ }
221
+
222
+ // Parse .kicad_sym file to extract installed components
223
+ async function parseSymbolLibrary(
224
+ filePath: string,
225
+ libraryName: string,
226
+ category: LibraryCategory,
227
+ models3dDir: string
228
+ ): Promise<InstalledComponent[]> {
229
+ const components: InstalledComponent[] = [];
230
+
231
+ try {
232
+ const content = await readFile(filePath, 'utf-8');
233
+
234
+ // Match symbol definitions: (symbol "JLC-MCP-Resistors:R_100k" ...)
235
+ const symbolPattern = /\(symbol\s+"([^"]+)"\s+/g;
236
+ const lcscPattern = /\(property\s+"LCSC"\s+"(C\d+)"/g;
237
+ const footprintPattern = /\(property\s+"Footprint"\s+"([^"]+)"/g;
238
+
239
+ // Split by symbol definitions to process each
240
+ const symbols = content.split(/(?=\(symbol\s+"[^"]+"\s+\()/);
241
+
242
+ for (const symbolBlock of symbols) {
243
+ // Skip header block
244
+ if (!symbolBlock.includes('(symbol "')) continue;
245
+
246
+ // Extract symbol name
247
+ const symbolMatch = symbolBlock.match(/\(symbol\s+"([^"]+)"/);
248
+ if (!symbolMatch) continue;
249
+
250
+ const fullSymbolRef = symbolMatch[1];
251
+ // Skip if this is a subsymbol (contains _1_1, _0_1, etc.)
252
+ if (fullSymbolRef.includes('_') && /_(0|1)_(0|1)$/.test(fullSymbolRef)) continue;
253
+
254
+ // Extract LCSC ID
255
+ const lcscMatch = symbolBlock.match(/\(property\s+"LCSC"\s+"(C\d+)"/);
256
+ const lcscId = lcscMatch ? lcscMatch[1] : '';
257
+
258
+ // Extract name from symbol ref (after the colon)
259
+ const nameParts = fullSymbolRef.split(':');
260
+ const name = nameParts.length > 1 ? nameParts[1] : fullSymbolRef;
261
+
262
+ // Extract footprint reference
263
+ const fpMatch = symbolBlock.match(/\(property\s+"Footprint"\s+"([^"]+)"/);
264
+ const footprintRef = fpMatch ? fpMatch[1] : '';
265
+
266
+ // Check for 3D model
267
+ const model3dPath = join(models3dDir, `${name}.step`);
268
+ const has3dModel = existsSync(model3dPath);
269
+
270
+ if (lcscId) {
271
+ components.push({
272
+ lcscId,
273
+ name,
274
+ category,
275
+ symbolRef: fullSymbolRef,
276
+ footprintRef,
277
+ library: libraryName,
278
+ has3dModel,
279
+ });
280
+ }
281
+ }
282
+ } catch {
283
+ // File read error - return empty
284
+ }
285
+
286
+ return components;
287
+ }
288
+
289
+ function adaptCommunityComponent(component: EasyEDACommunityComponent): EasyEDAComponentData {
290
+ const symbolHead = component.symbol.head as Record<string, unknown> | undefined;
291
+ const cPara = (symbolHead?.c_para as Record<string, string>) || {};
292
+
293
+ return {
294
+ info: {
295
+ name: component.title,
296
+ prefix: cPara.pre || 'U',
297
+ package: component.footprint.name,
298
+ manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer,
299
+ datasheet: cPara.link,
300
+ lcscId: undefined,
301
+ jlcId: undefined,
302
+ },
303
+ symbol: {
304
+ pins: component.symbol.pins,
305
+ rectangles: component.symbol.rectangles,
306
+ circles: component.symbol.circles,
307
+ ellipses: component.symbol.ellipses,
308
+ arcs: component.symbol.arcs,
309
+ polylines: component.symbol.polylines,
310
+ polygons: component.symbol.polygons,
311
+ paths: component.symbol.paths,
312
+ origin: component.symbol.origin,
313
+ },
314
+ footprint: {
315
+ name: component.footprint.name,
316
+ type: component.footprint.type,
317
+ pads: component.footprint.pads,
318
+ tracks: component.footprint.tracks,
319
+ holes: component.footprint.holes,
320
+ circles: component.footprint.circles,
321
+ arcs: component.footprint.arcs,
322
+ rects: component.footprint.rects,
323
+ texts: component.footprint.texts,
324
+ vias: component.footprint.vias,
325
+ solidRegions: component.footprint.solidRegions,
326
+ origin: component.footprint.origin,
327
+ },
328
+ model3d: component.model3d,
329
+ rawData: component.rawData,
330
+ };
331
+ }
332
+
333
+ export function createLibraryService(): LibraryService {
334
+ return {
335
+ async install(id: string, options: InstallOptions = {}): Promise<InstallResult> {
336
+ const isCommunityComponent = !isLcscId(id);
337
+
338
+ // Community components require projectPath
339
+ if (isCommunityComponent && !options.projectPath) {
340
+ throw new Error('EasyEDA community components require projectPath for local storage');
341
+ }
342
+
343
+ // Determine storage location
344
+ const isGlobal = !isCommunityComponent && !options.projectPath;
345
+ const paths = isGlobal
346
+ ? getGlobalLibraryPaths()
347
+ : getProjectLibraryPaths(options.projectPath!);
348
+
349
+ // Fetch component data
350
+ let component: EasyEDAComponentData | null = null;
351
+
352
+ if (isLcscId(id)) {
353
+ component = await easyedaClient.getComponentData(id);
354
+ } else {
355
+ const communityComponent = await easyedaCommunityClient.getComponent(id);
356
+ if (communityComponent) {
357
+ component = adaptCommunityComponent(communityComponent);
358
+ }
359
+ }
360
+
361
+ if (!component) {
362
+ throw new Error(`Component ${id} not found`);
363
+ }
364
+
365
+ // Enrich with JLC API data for LCSC components
366
+ if (!isCommunityComponent) {
367
+ try {
368
+ const jlcDetails = await jlcClient.getComponentDetails(id);
369
+ if (jlcDetails) {
370
+ if (jlcDetails.datasheetPdf) {
371
+ component.info.datasheetPdf = jlcDetails.datasheetPdf;
372
+ }
373
+ if (jlcDetails.description && jlcDetails.description !== jlcDetails.name) {
374
+ component.info.description = jlcDetails.description;
375
+ }
376
+ if (jlcDetails.attributes) {
377
+ component.info.attributes = {
378
+ ...component.info.attributes,
379
+ ...jlcDetails.attributes,
380
+ };
381
+ }
382
+ }
383
+ } catch {
384
+ // JLC enrichment is optional
385
+ }
386
+ }
387
+
388
+ // Variables for library paths and refs
389
+ let symbolFile: string;
390
+ let symbolName: string;
391
+ let symbolRef: string;
392
+ let footprintPath: string | undefined;
393
+ let footprintRef: string;
394
+ let footprintDir: string;
395
+ let models3dDir: string;
396
+ let category: string | undefined;
397
+ let modelPath: string | undefined;
398
+
399
+ if (isCommunityComponent) {
400
+ // EasyEDA community component → project-local EasyEDA library
401
+ const librariesDir = join(options.projectPath!, 'libraries');
402
+ const symbolsDir = join(librariesDir, 'symbols');
403
+ footprintDir = join(librariesDir, 'footprints', EASYEDA_FOOTPRINT_LIBRARY_NAME);
404
+ models3dDir = join(librariesDir, '3dmodels', 'EasyEDA.3dshapes');
405
+
406
+ await ensureDir(symbolsDir);
407
+ await ensureDir(footprintDir);
408
+
409
+ symbolFile = join(symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME);
410
+ symbolName = component.info.name.replace(/[^a-zA-Z0-9_-]/g, '_');
411
+
412
+ // Download 3D model first if available (needed for footprint generation)
413
+ // Community components include 3D by default
414
+ const include3d = options.include3d ?? true;
415
+ let modelRelativePath: string | undefined;
416
+
417
+ if (include3d && component.model3d) {
418
+ await ensureDir(models3dDir);
419
+ const model = await easyedaCommunityClient.get3DModel(component.model3d.uuid, 'step');
420
+ if (model) {
421
+ const modelFilename = `${symbolName}.step`;
422
+ modelPath = join(models3dDir, modelFilename);
423
+ await writeBinary(modelPath, model);
424
+ // Use relative path for project-local libraries
425
+ modelRelativePath = `\${KIPRJMOD}/libraries/3dmodels/EasyEDA.3dshapes/${modelFilename}`;
426
+ }
427
+ }
428
+
429
+ // Generate custom footprint with 3D model
430
+ const footprint = footprintConverter.convert(component, {
431
+ libraryName: EASYEDA_LIBRARY_NAME,
432
+ include3DModel: !!modelRelativePath,
433
+ modelPath: modelRelativePath,
434
+ });
435
+ footprintPath = join(footprintDir, `${symbolName}.kicad_mod`);
436
+ footprintRef = `${EASYEDA_LIBRARY_NAME}:${symbolName}`;
437
+ await writeText(footprintPath, footprint);
438
+
439
+ component.info.package = footprintRef;
440
+
441
+ // Update lib tables
442
+ await ensureSymLibTable(options.projectPath!, symbolFile, EASYEDA_LIBRARY_NAME, EASYEDA_LIBRARY_DESCRIPTION);
443
+ await ensureFpLibTable(options.projectPath!, footprintDir, EASYEDA_LIBRARY_NAME, EASYEDA_LIBRARY_DESCRIPTION);
444
+
445
+ symbolRef = `${EASYEDA_LIBRARY_NAME}:${symbolName}`;
446
+ } else {
447
+ // LCSC component → JLC-MCP category-based library
448
+ category = getLibraryCategory(
449
+ component.info.prefix,
450
+ component.info.category,
451
+ component.info.description
452
+ );
453
+
454
+ const symbolLibraryFilename = getLibraryFilename(category as LibraryCategory);
455
+ symbolFile = join(paths.symbolsDir, symbolLibraryFilename);
456
+ footprintDir = paths.footprintDir;
457
+ models3dDir = paths.models3dFullDir;
458
+
459
+ await ensureDir(paths.symbolsDir);
460
+ await ensureDir(paths.footprintDir);
461
+
462
+ // Pre-compute symbol name for 3D model path
463
+ symbolName = symbolConverter.getSymbolName(component);
464
+
465
+ // Download 3D model first if requested (needed for footprint generation)
466
+ const include3d = options.include3d ?? false;
467
+ let modelRelativePath: string | undefined;
468
+
469
+ if (include3d && component.model3d) {
470
+ await ensureDir(models3dDir);
471
+ const model = await easyedaClient.get3DModel(component.model3d.uuid, 'step');
472
+ if (model) {
473
+ const modelFilename = `${symbolName}.step`;
474
+ modelPath = join(models3dDir, modelFilename);
475
+ await writeBinary(modelPath, model);
476
+ // Use KiCad variable for portable path
477
+ modelRelativePath = `\${KICAD9_3RD_PARTY}/${LIBRARY_NAMESPACE}/3dmodels/${MODELS_3D_NAME}/${modelFilename}`;
478
+ }
479
+ }
480
+
481
+ // Determine footprint (may use KiCad standard)
482
+ const footprintResult = footprintConverter.getFootprint(component, {
483
+ include3DModel: !!modelRelativePath,
484
+ modelPath: modelRelativePath,
485
+ });
486
+
487
+ if (footprintResult.type === 'reference') {
488
+ footprintRef = footprintResult.reference!;
489
+ } else {
490
+ footprintPath = join(footprintDir, `${footprintResult.name}.kicad_mod`);
491
+ footprintRef = getFootprintReference(footprintResult.name);
492
+ await writeText(footprintPath, footprintResult.content!);
493
+ }
494
+
495
+ component.info.package = footprintRef;
496
+ symbolRef = getSymbolReference(category as LibraryCategory, symbolName);
497
+
498
+ // Update lib tables (project-local only)
499
+ if (!isGlobal && options.projectPath) {
500
+ await ensureSymLibTable(options.projectPath, symbolFile);
501
+ await ensureFpLibTable(options.projectPath, footprintDir);
502
+ }
503
+ }
504
+
505
+ // Handle symbol library (append, replace, or create)
506
+ let symbolContent: string;
507
+ let symbolAction: 'created' | 'appended' | 'exists' | 'replaced';
508
+
509
+ if (existsSync(symbolFile)) {
510
+ const existingContent = await readFile(symbolFile, 'utf-8');
511
+
512
+ if (symbolConverter.symbolExistsInLibrary(existingContent, component.info.name)) {
513
+ if (options.force) {
514
+ // Force reinstall - replace existing symbol
515
+ symbolContent = symbolConverter.replaceInLibrary(existingContent, component, {
516
+ libraryName: isCommunityComponent ? EASYEDA_LIBRARY_NAME : undefined,
517
+ symbolName: isCommunityComponent ? symbolName : undefined,
518
+ });
519
+ symbolAction = 'replaced';
520
+ } else {
521
+ symbolAction = 'exists';
522
+ symbolContent = existingContent;
523
+ }
524
+ } else {
525
+ symbolContent = symbolConverter.appendToLibrary(existingContent, component, {
526
+ libraryName: isCommunityComponent ? EASYEDA_LIBRARY_NAME : undefined,
527
+ symbolName: isCommunityComponent ? symbolName : undefined,
528
+ });
529
+ symbolAction = 'appended';
530
+ }
531
+ } else {
532
+ symbolContent = symbolConverter.convert(component, {
533
+ libraryName: isCommunityComponent ? EASYEDA_LIBRARY_NAME : undefined,
534
+ symbolName: isCommunityComponent ? symbolName : undefined,
535
+ });
536
+ symbolAction = 'created';
537
+ }
538
+
539
+ if (symbolAction !== 'exists') {
540
+ await writeText(symbolFile, symbolContent);
541
+ }
542
+
543
+ // Build validation data
544
+ const isKicadStandardFootprint = !isCommunityComponent && !footprintPath;
545
+ const footprintType = isKicadStandardFootprint ? 'reference' : 'generated';
546
+
547
+ const validationData: ValidationData = {
548
+ component: {
549
+ name: component.info.name,
550
+ description: component.info.description,
551
+ package: component.info.package,
552
+ manufacturer: component.info.manufacturer,
553
+ datasheet_url: component.info.datasheetPdf || component.info.datasheet,
554
+ },
555
+ symbol: {
556
+ pin_count: component.symbol.pins.length,
557
+ pins: component.symbol.pins.map(p => ({
558
+ number: p.number,
559
+ name: p.name,
560
+ electrical_type: p.electricalType,
561
+ })),
562
+ },
563
+ footprint: {
564
+ type: component.footprint.type,
565
+ pad_count: component.footprint.pads.length,
566
+ pads: footprintType === 'generated'
567
+ ? component.footprint.pads.map(p => ({
568
+ number: p.number,
569
+ shape: p.shape,
570
+ }))
571
+ : null,
572
+ is_kicad_standard: isKicadStandardFootprint,
573
+ kicad_ref: footprintRef,
574
+ },
575
+ checks: {
576
+ pin_pad_count_match: component.symbol.pins.length === component.footprint.pads.length,
577
+ has_power_pins: component.symbol.pins.some(p =>
578
+ p.electricalType === 'power_in' || p.electricalType === 'power_out'
579
+ ),
580
+ has_ground_pins: component.symbol.pins.some(p =>
581
+ p.name.toLowerCase().includes('gnd') || p.name.toLowerCase().includes('vss')
582
+ ),
583
+ },
584
+ };
585
+
586
+ return {
587
+ success: true,
588
+ id,
589
+ source: isCommunityComponent ? 'easyeda_community' : 'lcsc',
590
+ storageMode: isGlobal ? 'global' : 'project-local',
591
+ category,
592
+ symbolName,
593
+ symbolRef,
594
+ footprintRef,
595
+ footprintType,
596
+ datasheet: component.info.datasheet || (!isCommunityComponent ? `https://www.lcsc.com/datasheet/lcsc_datasheet_${id}.pdf` : undefined),
597
+ files: {
598
+ symbolLibrary: symbolFile,
599
+ footprint: footprintPath,
600
+ model3d: modelPath,
601
+ },
602
+ symbolAction,
603
+ validationData,
604
+ };
605
+ },
606
+
607
+ async listInstalled(options: ListOptions = {}): Promise<InstalledComponent[]> {
608
+ const paths = options.projectPath
609
+ ? getProjectLibraryPaths(options.projectPath)
610
+ : getGlobalLibraryPaths();
611
+
612
+ const allComponents: InstalledComponent[] = [];
613
+ const categories = getAllCategories();
614
+
615
+ for (const category of categories) {
616
+ // Filter by category if specified
617
+ if (options.category && options.category !== category) continue;
618
+
619
+ const libraryFilename = getLibraryFilename(category);
620
+ const libraryPath = join(paths.symbolsDir, libraryFilename);
621
+ const libraryName = `JLC-MCP-${category}`;
622
+
623
+ if (existsSync(libraryPath)) {
624
+ const components = await parseSymbolLibrary(
625
+ libraryPath,
626
+ libraryName,
627
+ category,
628
+ paths.models3dFullDir
629
+ );
630
+ allComponents.push(...components);
631
+ }
632
+ }
633
+
634
+ return allComponents;
635
+ },
636
+
637
+ async update(_options: UpdateOptions = {}): Promise<UpdateResult> {
638
+ // TODO: Implement by re-fetching all components in a library
639
+ return { updated: 0, failed: 0, skipped: 0, components: [] };
640
+ },
641
+
642
+ async ensureGlobalTables(): Promise<void> {
643
+ const { ensureGlobalLibraryTables } = await import('../converter/global-lib-table.js');
644
+ await ensureGlobalLibraryTables();
645
+ },
646
+
647
+ async getStatus(): Promise<LibraryStatus> {
648
+ const version = detectKicadVersion();
649
+ const paths = getGlobalLibraryPaths();
650
+ const configDir = getKicadConfigDir(version);
651
+
652
+ const symLibTablePath = join(configDir, 'sym-lib-table');
653
+ const fpLibTablePath = join(configDir, 'fp-lib-table');
654
+
655
+ // Check if library directories exist with actual content
656
+ let installed = false;
657
+ if (existsSync(paths.symbolsDir)) {
658
+ try {
659
+ const files = await readdir(paths.symbolsDir);
660
+ installed = files.some(f => f.endsWith('.kicad_sym'));
661
+ } catch {
662
+ installed = false;
663
+ }
664
+ }
665
+
666
+ // Check if libraries are linked in KiCad tables
667
+ let linked = false;
668
+ if (existsSync(symLibTablePath)) {
669
+ try {
670
+ const content = await readFile(symLibTablePath, 'utf-8');
671
+ linked = content.includes('JLC-MCP');
672
+ } catch {
673
+ linked = false;
674
+ }
675
+ }
676
+
677
+ // Count installed components
678
+ const components = await this.listInstalled({});
679
+ const componentCount = components.length;
680
+
681
+ return {
682
+ installed,
683
+ linked,
684
+ version,
685
+ componentCount,
686
+ paths: {
687
+ symbolsDir: paths.symbolsDir,
688
+ footprintsDir: paths.footprintsDir,
689
+ models3dDir: paths.models3dFullDir,
690
+ symLibTable: symLibTablePath,
691
+ fpLibTable: fpLibTablePath,
692
+ },
693
+ };
694
+ },
695
+ };
696
+ }