@jlcpcb/core 0.1.0 → 0.2.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 (114) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/api/easyeda-community.d.ts +36 -0
  3. package/dist/api/easyeda-community.d.ts.map +1 -0
  4. package/dist/api/easyeda.d.ts +23 -0
  5. package/dist/api/easyeda.d.ts.map +1 -0
  6. package/dist/api/index.d.ts +7 -0
  7. package/dist/api/index.d.ts.map +1 -0
  8. package/dist/api/jlc.d.ts +41 -0
  9. package/dist/api/jlc.d.ts.map +1 -0
  10. package/dist/assets/search.html +528 -0
  11. package/dist/browser/index.d.ts +8 -0
  12. package/dist/browser/index.d.ts.map +1 -0
  13. package/dist/browser/kicad-renderer.d.ts +13 -0
  14. package/dist/browser/kicad-renderer.d.ts.map +1 -0
  15. package/dist/browser/sexpr-parser.d.ts +84 -0
  16. package/dist/browser/sexpr-parser.d.ts.map +1 -0
  17. package/dist/constants/design-rules.d.ts +34 -0
  18. package/dist/constants/design-rules.d.ts.map +1 -0
  19. package/dist/constants/footprints.d.ts +134 -0
  20. package/dist/constants/footprints.d.ts.map +1 -0
  21. package/dist/constants/index.d.ts +7 -0
  22. package/dist/constants/index.d.ts.map +1 -0
  23. package/dist/constants/kicad.d.ts +67 -0
  24. package/dist/constants/kicad.d.ts.map +1 -0
  25. package/dist/converter/category-router.d.ts +47 -0
  26. package/dist/converter/category-router.d.ts.map +1 -0
  27. package/dist/converter/footprint-mapper.d.ts +40 -0
  28. package/dist/converter/footprint-mapper.d.ts.map +1 -0
  29. package/dist/converter/footprint-mapper.test.d.ts +2 -0
  30. package/dist/converter/footprint-mapper.test.d.ts.map +1 -0
  31. package/dist/converter/footprint.d.ts +116 -0
  32. package/dist/converter/footprint.d.ts.map +1 -0
  33. package/dist/converter/global-lib-table.d.ts +29 -0
  34. package/dist/converter/global-lib-table.d.ts.map +1 -0
  35. package/dist/converter/index.d.ts +12 -0
  36. package/dist/converter/index.d.ts.map +1 -0
  37. package/dist/converter/lib-table.d.ts +61 -0
  38. package/dist/converter/lib-table.d.ts.map +1 -0
  39. package/dist/converter/svg-arc.d.ts +45 -0
  40. package/dist/converter/svg-arc.d.ts.map +1 -0
  41. package/dist/converter/symbol-templates.d.ts +34 -0
  42. package/dist/converter/symbol-templates.d.ts.map +1 -0
  43. package/dist/converter/symbol.d.ts +223 -0
  44. package/dist/converter/symbol.d.ts.map +1 -0
  45. package/dist/converter/value-normalizer.d.ts +33 -0
  46. package/dist/converter/value-normalizer.d.ts.map +1 -0
  47. package/dist/http/index.d.ts +5 -0
  48. package/dist/http/index.d.ts.map +1 -0
  49. package/dist/http/routes.d.ts +12 -0
  50. package/dist/http/routes.d.ts.map +1 -0
  51. package/dist/http/server.d.ts +18 -0
  52. package/dist/http/server.d.ts.map +1 -0
  53. package/dist/index.d.ts +13 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +9633 -0
  56. package/dist/parsers/easyeda-shapes.d.ts +115 -0
  57. package/dist/parsers/easyeda-shapes.d.ts.map +1 -0
  58. package/dist/parsers/http-client.d.ts +16 -0
  59. package/dist/parsers/http-client.d.ts.map +1 -0
  60. package/dist/parsers/index.d.ts +11 -0
  61. package/dist/parsers/index.d.ts.map +1 -0
  62. package/dist/parsers/utils.d.ts +17 -0
  63. package/dist/parsers/utils.d.ts.map +1 -0
  64. package/dist/services/component-service.d.ts +31 -0
  65. package/dist/services/component-service.d.ts.map +1 -0
  66. package/dist/services/fix-service.d.ts +40 -0
  67. package/dist/services/fix-service.d.ts.map +1 -0
  68. package/dist/services/index.d.ts +8 -0
  69. package/dist/services/index.d.ts.map +1 -0
  70. package/dist/services/library-service.d.ts +112 -0
  71. package/dist/services/library-service.d.ts.map +1 -0
  72. package/dist/types/component.d.ts +56 -0
  73. package/dist/types/component.d.ts.map +1 -0
  74. package/dist/types/easyeda-community.d.ts +74 -0
  75. package/dist/types/easyeda-community.d.ts.map +1 -0
  76. package/dist/types/easyeda.d.ts +326 -0
  77. package/dist/types/easyeda.d.ts.map +1 -0
  78. package/dist/types/index.d.ts +12 -0
  79. package/dist/types/index.d.ts.map +1 -0
  80. package/dist/types/jlc.d.ts +78 -0
  81. package/dist/types/jlc.d.ts.map +1 -0
  82. package/dist/types/kicad.d.ts +141 -0
  83. package/dist/types/kicad.d.ts.map +1 -0
  84. package/dist/types/mcp.d.ts +66 -0
  85. package/dist/types/mcp.d.ts.map +1 -0
  86. package/dist/types/project.d.ts +60 -0
  87. package/dist/types/project.d.ts.map +1 -0
  88. package/dist/utils/conversion.d.ts +59 -0
  89. package/dist/utils/conversion.d.ts.map +1 -0
  90. package/dist/utils/file-system.d.ts +59 -0
  91. package/dist/utils/file-system.d.ts.map +1 -0
  92. package/dist/utils/index.d.ts +8 -0
  93. package/dist/utils/index.d.ts.map +1 -0
  94. package/dist/utils/logger.d.ts +26 -0
  95. package/dist/utils/logger.d.ts.map +1 -0
  96. package/dist/utils/validation.d.ts +259 -0
  97. package/dist/utils/validation.d.ts.map +1 -0
  98. package/package.json +5 -3
  99. package/scripts/build-search-page.ts +68 -0
  100. package/src/assets/search-built.html +528 -0
  101. package/src/assets/search.html +458 -0
  102. package/src/browser/index.ts +389 -0
  103. package/src/browser/kicad-renderer.ts +813 -0
  104. package/src/browser/sexpr-parser.ts +333 -0
  105. package/src/converter/footprint-mapper.test.ts +159 -0
  106. package/src/converter/footprint-mapper.ts +42 -134
  107. package/src/converter/footprint.ts +208 -36
  108. package/src/converter/global-lib-table.ts +71 -0
  109. package/src/http/index.ts +5 -0
  110. package/src/http/routes.ts +266 -0
  111. package/src/http/server.ts +83 -0
  112. package/src/index.ts +3 -0
  113. package/src/parsers/easyeda-shapes.ts +2 -1
  114. package/src/services/library-service.ts +73 -22
@@ -0,0 +1,266 @@
1
+ /**
2
+ * HTTP routes for the EasyEDA Component Browser
3
+ * Handles serving the HTML page and proxying API requests
4
+ *
5
+ * Returns KiCad S-expression strings for symbol/footprint preview rendering
6
+ */
7
+
8
+ import { readFileSync } from 'fs'
9
+ import { join, dirname } from 'path'
10
+ import { fileURLToPath } from 'url'
11
+ import type { IncomingMessage, ServerResponse } from 'http'
12
+ import { createLogger } from '../utils/logger.js'
13
+ import type { EasyEDAComponentData, EasyEDACommunityComponent } from '../types/index.js'
14
+ import { easyedaCommunityClient } from '../api/easyeda-community.js'
15
+ import { symbolConverter } from '../converter/symbol.js'
16
+ import { footprintConverter } from '../converter/footprint.js'
17
+
18
+ const logger = createLogger('http-routes')
19
+
20
+ // Get the directory of this file to find assets
21
+ const __filename = fileURLToPath(import.meta.url)
22
+ const __dirname = dirname(__filename)
23
+
24
+ // Cache the HTML content in memory
25
+ let htmlCache: string | null = null
26
+
27
+ /**
28
+ * Load the HTML page from the assets directory
29
+ */
30
+ function getHtmlPage(): string {
31
+ if (htmlCache) return htmlCache
32
+
33
+ try {
34
+ // When bundled, __dirname points to dist/ since everything is in index.js
35
+ // When running from source, __dirname is src/http/
36
+ const possiblePaths = [
37
+ // Bundled: dist/index.js looking for dist/assets/search.html
38
+ join(__dirname, 'assets/search.html'),
39
+ // Bundled alternative path
40
+ join(__dirname, '../dist/assets/search.html'),
41
+ // Source: src/http/ looking for src/assets/
42
+ join(__dirname, '../assets/search.html'),
43
+ join(__dirname, '../assets/search-built.html'),
44
+ // From project root (core package)
45
+ join(process.cwd(), 'dist/assets/search.html'),
46
+ join(process.cwd(), 'packages/core/dist/assets/search.html'),
47
+ // When imported from other packages
48
+ join(__dirname, '../../dist/assets/search.html'),
49
+ // When bundled into CLI or other package, look for core's assets relative to monorepo
50
+ join(__dirname, '../../../core/dist/assets/search.html'),
51
+ join(__dirname, '../../../../packages/core/dist/assets/search.html'),
52
+ // Look in node_modules if installed as dependency
53
+ join(__dirname, '../../node_modules/@jlcpcb/core/dist/assets/search.html'),
54
+ ]
55
+
56
+ for (const path of possiblePaths) {
57
+ try {
58
+ htmlCache = readFileSync(path, 'utf-8')
59
+ logger.debug(`Loaded HTML from: ${path}`)
60
+ return htmlCache
61
+ } catch {
62
+ // Try next path
63
+ }
64
+ }
65
+
66
+ throw new Error('HTML file not found')
67
+ } catch (error) {
68
+ logger.error('Failed to load HTML page:', error)
69
+ return `<!DOCTYPE html>
70
+ <html>
71
+ <head><title>Error</title></head>
72
+ <body>
73
+ <h1>Error: Search page not found</h1>
74
+ <p>The search page has not been built. Run: bun run build</p>
75
+ </body>
76
+ </html>`
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Main request handler
82
+ */
83
+ export async function handleRequest(
84
+ req: IncomingMessage,
85
+ res: ServerResponse
86
+ ): Promise<void> {
87
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
88
+ const pathname = url.pathname
89
+
90
+ logger.debug(`${req.method} ${pathname}`)
91
+
92
+ // Route requests
93
+ if (pathname === '/' || pathname === '/index.html') {
94
+ const query = url.searchParams.get('q') || undefined
95
+ serveHtml(res, query)
96
+ } else if (pathname === '/api/search') {
97
+ await handleSearch(url, res)
98
+ } else if (pathname.startsWith('/api/component/')) {
99
+ const uuid = pathname.replace('/api/component/', '')
100
+ await handleComponent(uuid, res)
101
+ } else if (pathname === '/health') {
102
+ res.writeHead(200, { 'Content-Type': 'application/json' })
103
+ res.end(JSON.stringify({ status: 'ok' }))
104
+ } else {
105
+ res.writeHead(404, { 'Content-Type': 'application/json' })
106
+ res.end(JSON.stringify({ error: 'Not found' }))
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Serve the HTML page with optional initial query injection
112
+ */
113
+ function serveHtml(res: ServerResponse, initialQuery?: string): void {
114
+ let html = getHtmlPage()
115
+
116
+ if (initialQuery) {
117
+ // Inject initial query via a script tag before </head>
118
+ const script = `<script>window.__INITIAL_QUERY__ = ${JSON.stringify(initialQuery)};</script>`
119
+ html = html.replace('</head>', `${script}</head>`)
120
+ }
121
+
122
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
123
+ res.end(html)
124
+ }
125
+
126
+ /**
127
+ * Handle search API requests
128
+ * GET /api/search?q=query&source=user|lcsc|all&page=1&limit=20
129
+ */
130
+ async function handleSearch(
131
+ url: URL,
132
+ res: ServerResponse
133
+ ): Promise<void> {
134
+ const query = url.searchParams.get('q') || ''
135
+ const source = url.searchParams.get('source') || 'user'
136
+ const page = parseInt(url.searchParams.get('page') || '1', 10)
137
+ const limit = parseInt(url.searchParams.get('limit') || '20', 10)
138
+
139
+ if (!query) {
140
+ res.writeHead(400, { 'Content-Type': 'application/json' })
141
+ res.end(JSON.stringify({ error: 'Missing query parameter' }))
142
+ return
143
+ }
144
+
145
+ try {
146
+ // Fetch more than we need for pagination (EasyEDA doesn't support offset)
147
+ const allResults = await easyedaCommunityClient.search({
148
+ query,
149
+ source: source as 'user' | 'lcsc' | 'easyeda' | 'all',
150
+ limit: Math.min(limit * page + limit, 100), // Max 100 results
151
+ })
152
+
153
+ // Calculate pagination
154
+ const startIndex = (page - 1) * limit
155
+ const endIndex = startIndex + limit
156
+ const results = allResults.slice(startIndex, endIndex)
157
+ const totalPages = Math.ceil(allResults.length / limit)
158
+
159
+ res.writeHead(200, { 'Content-Type': 'application/json' })
160
+ res.end(JSON.stringify({
161
+ results,
162
+ pagination: {
163
+ page,
164
+ limit,
165
+ total: allResults.length,
166
+ totalPages,
167
+ hasNext: page < totalPages,
168
+ hasPrev: page > 1,
169
+ },
170
+ }))
171
+ } catch (error) {
172
+ logger.error('Search error:', error)
173
+ res.writeHead(500, { 'Content-Type': 'application/json' })
174
+ res.end(JSON.stringify({ error: 'Search failed' }))
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Convert EasyEDACommunityComponent to EasyEDAComponentData format
180
+ * Required because converters expect the LCSC-style structure
181
+ */
182
+ function communityToComponentData(
183
+ community: EasyEDACommunityComponent
184
+ ): EasyEDAComponentData {
185
+ // Extract component info from the head c_para
186
+ const cPara = community.symbol.head?.c_para as Record<string, string> | undefined ?? {}
187
+ const fpCPara = community.footprint.head?.c_para as Record<string, string> | undefined ?? {}
188
+
189
+ return {
190
+ info: {
191
+ name: community.title || cPara.name || 'Unknown',
192
+ prefix: cPara.pre || cPara.Prefix || 'U',
193
+ package: fpCPara.package || community.footprint.name,
194
+ manufacturer: cPara.Manufacturer || cPara.BOM_Manufacturer,
195
+ description: community.description || cPara.BOM_Manufacturer_Part,
196
+ category: cPara.package, // Best guess for category
197
+ },
198
+ symbol: community.symbol,
199
+ footprint: community.footprint,
200
+ model3d: community.model3d,
201
+ rawData: community.rawData,
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Handle component fetch API requests
207
+ * GET /api/component/:uuid
208
+ *
209
+ * Returns KiCad S-expression strings for browser-side rendering
210
+ */
211
+ async function handleComponent(
212
+ uuid: string,
213
+ res: ServerResponse
214
+ ): Promise<void> {
215
+ if (!uuid) {
216
+ res.writeHead(400, { 'Content-Type': 'application/json' })
217
+ res.end(JSON.stringify({ error: 'Missing UUID' }))
218
+ return
219
+ }
220
+
221
+ try {
222
+ const component = await easyedaCommunityClient.getComponent(uuid)
223
+
224
+ if (!component) {
225
+ res.writeHead(404, { 'Content-Type': 'application/json' })
226
+ res.end(JSON.stringify({ error: 'Component not found' }))
227
+ return
228
+ }
229
+
230
+ // Convert community component to converter-compatible format
231
+ const componentData = communityToComponentData(component)
232
+
233
+ // Generate KiCad S-expression strings using existing converters
234
+ let symbolSexpr = ''
235
+ let footprintSexpr = ''
236
+
237
+ try {
238
+ // Get just the symbol entry (not the full library wrapper)
239
+ symbolSexpr = symbolConverter.convertToSymbolEntry(componentData)
240
+ } catch (e) {
241
+ logger.warn('Symbol conversion failed:', e)
242
+ }
243
+
244
+ try {
245
+ footprintSexpr = footprintConverter.convert(componentData)
246
+ } catch (e) {
247
+ logger.warn('Footprint conversion failed:', e)
248
+ }
249
+
250
+ // Return S-expr strings for browser rendering
251
+ res.writeHead(200, { 'Content-Type': 'application/json' })
252
+ res.end(JSON.stringify({
253
+ uuid: component.uuid,
254
+ title: component.title,
255
+ description: component.description,
256
+ owner: component.owner,
257
+ symbolSexpr,
258
+ footprintSexpr,
259
+ model3d: component.model3d,
260
+ }))
261
+ } catch (error) {
262
+ logger.error('Component fetch error:', error)
263
+ res.writeHead(500, { 'Content-Type': 'application/json' })
264
+ res.end(JSON.stringify({ error: 'Failed to fetch component' }))
265
+ }
266
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * HTTP server for the EasyEDA Component Browser
3
+ * Runs alongside the MCP stdio server to serve the web UI and proxy API requests
4
+ */
5
+
6
+ import { createServer, type IncomingMessage, type ServerResponse } from 'http'
7
+ import { createLogger } from '../utils/logger.js'
8
+ import { handleRequest } from './routes.js'
9
+
10
+ const logger = createLogger('http-server')
11
+
12
+ const DEFAULT_PORT = 3847
13
+
14
+ let serverInstance: ReturnType<typeof createServer> | null = null
15
+
16
+ export interface HttpServerOptions {
17
+ port?: number
18
+ onReady?: (url: string) => void
19
+ }
20
+
21
+ /**
22
+ * Start the HTTP server
23
+ * @returns The port the server is listening on
24
+ */
25
+ export function startHttpServer(options: HttpServerOptions = {}): number {
26
+ if (serverInstance) {
27
+ logger.debug('HTTP server already running')
28
+ const port = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10)
29
+ options.onReady?.(`http://localhost:${port}`)
30
+ return port
31
+ }
32
+
33
+ const port = options.port ?? parseInt(process.env.JLC_MCP_HTTP_PORT || String(DEFAULT_PORT), 10)
34
+
35
+ serverInstance = createServer(async (req: IncomingMessage, res: ServerResponse) => {
36
+ // Add CORS headers to all responses
37
+ res.setHeader('Access-Control-Allow-Origin', '*')
38
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
39
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
40
+
41
+ // Handle preflight requests
42
+ if (req.method === 'OPTIONS') {
43
+ res.writeHead(204)
44
+ res.end()
45
+ return
46
+ }
47
+
48
+ try {
49
+ await handleRequest(req, res)
50
+ } catch (error) {
51
+ logger.error('Request error:', error)
52
+ res.writeHead(500, { 'Content-Type': 'application/json' })
53
+ res.end(JSON.stringify({ error: 'Internal server error' }))
54
+ }
55
+ })
56
+
57
+ serverInstance.listen(port, () => {
58
+ const url = `http://localhost:${port}`
59
+ logger.info(`HTTP server listening on ${url}`)
60
+ options.onReady?.(url)
61
+ })
62
+
63
+ serverInstance.on('error', (error: NodeJS.ErrnoException) => {
64
+ if (error.code === 'EADDRINUSE') {
65
+ logger.warn(`Port ${port} already in use, HTTP server not started`)
66
+ } else {
67
+ logger.error('HTTP server error:', error)
68
+ }
69
+ })
70
+
71
+ return port
72
+ }
73
+
74
+ /**
75
+ * Stop the HTTP server
76
+ */
77
+ export function stopHttpServer(): void {
78
+ if (serverInstance) {
79
+ serverInstance.close()
80
+ serverInstance = null
81
+ logger.info('HTTP server stopped')
82
+ }
83
+ }
package/src/index.ts CHANGED
@@ -23,3 +23,6 @@ export * from './constants/index.js';
23
23
 
24
24
  // Parsers
25
25
  export * from './parsers/index.js';
26
+
27
+ // HTTP Server
28
+ export * from './http/index.js';
@@ -609,7 +609,8 @@ export function parseFootprintShapes(shapes: string[]): ParsedFootprintData {
609
609
  }
610
610
 
611
611
  // Determine type based on pads
612
- const type = pads.some(p => p.holeRadius > 0) ? 'tht' : 'smd';
612
+ // Check both holeRadius > 0 and isPlated polygon pads (EasyEDA often has holeRadius=0 for polygon THT)
613
+ const type = pads.some(p => p.holeRadius > 0 || (p.shape === 'POLYGON' && p.isPlated)) ? 'tht' : 'smd';
613
614
 
614
615
  return {
615
616
  name: 'Unknown', // Will be set by caller
@@ -38,12 +38,18 @@ const LIBRARY_NAMESPACE = 'jlc_mcp';
38
38
  // KiCad versions to check
39
39
  const KICAD_VERSIONS = ['9.0', '8.0'];
40
40
 
41
- // EasyEDA community library naming
41
+ // EasyEDA community library naming (global)
42
42
  const EASYEDA_LIBRARY_NAME = 'EasyEDA';
43
43
  const EASYEDA_SYMBOL_LIBRARY_NAME = 'EasyEDA.kicad_sym';
44
44
  const EASYEDA_FOOTPRINT_LIBRARY_NAME = 'EasyEDA.pretty';
45
45
  const EASYEDA_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library';
46
46
 
47
+ // EasyEDA community library naming (project-local) - different name to avoid collision
48
+ const EASYEDA_LOCAL_LIBRARY_NAME = 'EasyEDA-local';
49
+ const EASYEDA_LOCAL_SYMBOL_LIBRARY_NAME = 'EasyEDA-local.kicad_sym';
50
+ const EASYEDA_LOCAL_FOOTPRINT_LIBRARY_NAME = 'EasyEDA-local.pretty';
51
+ const EASYEDA_LOCAL_LIBRARY_DESCRIPTION = 'EasyEDA Community Component Library (Project-local)';
52
+
47
53
  export interface InstallOptions {
48
54
  projectPath?: string;
49
55
  include3d?: boolean;
@@ -144,6 +150,7 @@ export interface LibraryService {
144
150
  update(options?: UpdateOptions): Promise<UpdateResult>;
145
151
  ensureGlobalTables(): Promise<void>;
146
152
  getStatus(): Promise<LibraryStatus>;
153
+ isEasyEDAInstalled(componentName: string): Promise<boolean>;
147
154
  }
148
155
 
149
156
  interface LibraryPaths {
@@ -335,13 +342,8 @@ export function createLibraryService(): LibraryService {
335
342
  async install(id: string, options: InstallOptions = {}): Promise<InstallResult> {
336
343
  const isCommunityComponent = !isLcscId(id);
337
344
 
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
+ // Determine storage location (global if no projectPath provided)
346
+ const isGlobal = !options.projectPath;
345
347
  const paths = isGlobal
346
348
  ? getGlobalLibraryPaths()
347
349
  : getProjectLibraryPaths(options.projectPath!);
@@ -397,16 +399,35 @@ export function createLibraryService(): LibraryService {
397
399
  let modelPath: string | undefined;
398
400
 
399
401
  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');
402
+ // EasyEDA community component → EasyEDA library (global or project-local)
403
+ // Use different library names to avoid collision
404
+ const libName = isGlobal ? EASYEDA_LIBRARY_NAME : EASYEDA_LOCAL_LIBRARY_NAME;
405
+ const symLibFile = isGlobal ? EASYEDA_SYMBOL_LIBRARY_NAME : EASYEDA_LOCAL_SYMBOL_LIBRARY_NAME;
406
+ const fpLibDir = isGlobal ? EASYEDA_FOOTPRINT_LIBRARY_NAME : EASYEDA_LOCAL_FOOTPRINT_LIBRARY_NAME;
407
+ const libDesc = isGlobal ? EASYEDA_LIBRARY_DESCRIPTION : EASYEDA_LOCAL_LIBRARY_DESCRIPTION;
408
+ const models3dDirName = isGlobal ? 'EasyEDA.3dshapes' : 'EasyEDA-local.3dshapes';
409
+
410
+ let symbolsDir: string;
411
+ let easyedaModelsDir: string;
412
+
413
+ if (isGlobal) {
414
+ // Global: use KiCad 3rd party directory
415
+ symbolsDir = paths.symbolsDir;
416
+ footprintDir = join(paths.footprintsDir, fpLibDir);
417
+ easyedaModelsDir = join(paths.models3dDir, models3dDirName);
418
+ } else {
419
+ // Project-local
420
+ const librariesDir = join(options.projectPath!, 'libraries');
421
+ symbolsDir = join(librariesDir, 'symbols');
422
+ footprintDir = join(librariesDir, 'footprints', fpLibDir);
423
+ easyedaModelsDir = join(librariesDir, '3dmodels', models3dDirName);
424
+ }
425
+ models3dDir = easyedaModelsDir;
405
426
 
406
427
  await ensureDir(symbolsDir);
407
428
  await ensureDir(footprintDir);
408
429
 
409
- symbolFile = join(symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME);
430
+ symbolFile = join(symbolsDir, symLibFile);
410
431
  symbolName = component.info.name.replace(/[^a-zA-Z0-9_-]/g, '_');
411
432
 
412
433
  // Download 3D model first if available (needed for footprint generation)
@@ -421,28 +442,38 @@ export function createLibraryService(): LibraryService {
421
442
  const modelFilename = `${symbolName}.step`;
422
443
  modelPath = join(models3dDir, modelFilename);
423
444
  await writeBinary(modelPath, model);
424
- // Use relative path for project-local libraries
425
- modelRelativePath = `\${KIPRJMOD}/libraries/3dmodels/EasyEDA.3dshapes/${modelFilename}`;
445
+ // Use appropriate path variable
446
+ if (isGlobal) {
447
+ modelRelativePath = `\${KICAD9_3RD_PARTY}/${LIBRARY_NAMESPACE}/3dmodels/${models3dDirName}/${modelFilename}`;
448
+ } else {
449
+ modelRelativePath = `\${KIPRJMOD}/libraries/3dmodels/${models3dDirName}/${modelFilename}`;
450
+ }
426
451
  }
427
452
  }
428
453
 
429
454
  // Generate custom footprint with 3D model
430
455
  const footprint = footprintConverter.convert(component, {
431
- libraryName: EASYEDA_LIBRARY_NAME,
456
+ libraryName: libName,
432
457
  include3DModel: !!modelRelativePath,
433
458
  modelPath: modelRelativePath,
434
459
  });
435
460
  footprintPath = join(footprintDir, `${symbolName}.kicad_mod`);
436
- footprintRef = `${EASYEDA_LIBRARY_NAME}:${symbolName}`;
461
+ footprintRef = `${libName}:${symbolName}`;
437
462
  await writeText(footprintPath, footprint);
438
463
 
439
464
  component.info.package = footprintRef;
440
465
 
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);
466
+ // Update lib tables (global or project-local)
467
+ if (isGlobal) {
468
+ // For global, use the global lib table registration
469
+ const { ensureGlobalEasyEDALibrary } = await import('../converter/global-lib-table.js');
470
+ await ensureGlobalEasyEDALibrary();
471
+ } else {
472
+ await ensureSymLibTable(options.projectPath!, symbolFile, libName, libDesc);
473
+ await ensureFpLibTable(options.projectPath!, footprintDir, libName, libDesc);
474
+ }
444
475
 
445
- symbolRef = `${EASYEDA_LIBRARY_NAME}:${symbolName}`;
476
+ symbolRef = `${libName}:${symbolName}`;
446
477
  } else {
447
478
  // LCSC component → JLC-MCP category-based library
448
479
  category = getLibraryCategory(
@@ -692,5 +723,25 @@ export function createLibraryService(): LibraryService {
692
723
  },
693
724
  };
694
725
  },
726
+
727
+ async isEasyEDAInstalled(componentName: string): Promise<boolean> {
728
+ const paths = getGlobalLibraryPaths();
729
+ const easyedaLibPath = join(paths.symbolsDir, EASYEDA_SYMBOL_LIBRARY_NAME);
730
+
731
+ if (!existsSync(easyedaLibPath)) {
732
+ return false;
733
+ }
734
+
735
+ try {
736
+ const content = await readFile(easyedaLibPath, 'utf-8');
737
+ // Sanitize the name the same way we do when installing
738
+ const sanitizedName = componentName.replace(/[^a-zA-Z0-9_-]/g, '_');
739
+ // Check if symbol exists in library
740
+ const pattern = new RegExp(`\\(symbol\\s+"${sanitizedName}"`, 'm');
741
+ return pattern.test(content);
742
+ } catch {
743
+ return false;
744
+ }
745
+ },
695
746
  };
696
747
  }