ecadlib 0.3.0__py3-none-any.whl

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 (199) hide show
  1. ecadlib/__init__.py +3 -0
  2. ecadlib/__main__.py +5 -0
  3. ecadlib/a365_sync.py +637 -0
  4. ecadlib/agent.py +1407 -0
  5. ecadlib/agent_queue.py +780 -0
  6. ecadlib/agent_watch.py +732 -0
  7. ecadlib/altium365.py +385 -0
  8. ecadlib/altium_dblib_sync.py +530 -0
  9. ecadlib/altium_export.py +209 -0
  10. ecadlib/altium_forms.py +768 -0
  11. ecadlib/altium_lib.py +1089 -0
  12. ecadlib/altium_menu.py +1751 -0
  13. ecadlib/altium_pkg.py +396 -0
  14. ecadlib/altium_project.py +1095 -0
  15. ecadlib/app.py +1458 -0
  16. ecadlib/approvals.py +395 -0
  17. ecadlib/assets/demo_bom.csv +8 -0
  18. ecadlib/assets/kicad_action_plugin.py +607 -0
  19. ecadlib/assets/kicad_reread.py +60 -0
  20. ecadlib/assets/pcblib_template.json.gz +0 -0
  21. ecadlib/bom_inputs.py +339 -0
  22. ecadlib/bom_triage.py +519 -0
  23. ecadlib/cad_model.py +528 -0
  24. ecadlib/cfb.py +171 -0
  25. ecadlib/cli.py +185 -0
  26. ecadlib/clihelp.py +149 -0
  27. ecadlib/cliutil.py +119 -0
  28. ecadlib/completion.py +173 -0
  29. ecadlib/costs.py +276 -0
  30. ecadlib/crosscheck.py +412 -0
  31. ecadlib/datasheet.py +2168 -0
  32. ecadlib/dedupe.py +572 -0
  33. ecadlib/detect.py +170 -0
  34. ecadlib/doctor.py +450 -0
  35. ecadlib/ds_route.py +541 -0
  36. ecadlib/eagle_export.py +1409 -0
  37. ecadlib/easy_setup.py +338 -0
  38. ecadlib/easyeda_convert.py +386 -0
  39. ecadlib/easyeda_pro_export.py +972 -0
  40. ecadlib/eval/__init__.py +0 -0
  41. ecadlib/eval/baselines/kicad-v1.json +404 -0
  42. ecadlib/eval/build_set.py +299 -0
  43. ecadlib/eval/cli.py +119 -0
  44. ecadlib/eval/gate.py +358 -0
  45. ecadlib/eval/golden/demo.txt +379 -0
  46. ecadlib/eval/golden/specs.txt +3120 -0
  47. ecadlib/eval/golden/testpack.txt +463 -0
  48. ecadlib/eval/golden.py +446 -0
  49. ecadlib/eval/replay/kicad-v1.json.gz +0 -0
  50. ecadlib/eval/runner.py +456 -0
  51. ecadlib/eval/score.py +179 -0
  52. ecadlib/eval/sets/kicad-v1.json +630 -0
  53. ecadlib/firstrun.py +400 -0
  54. ecadlib/footprint_adv.py +1006 -0
  55. ecadlib/footprint_bench.py +218 -0
  56. ecadlib/footprint_check.py +562 -0
  57. ecadlib/footprint_gen.py +582 -0
  58. ecadlib/footprint_rules.py +1092 -0
  59. ecadlib/forge.py +267 -0
  60. ecadlib/fp_share.py +95 -0
  61. ecadlib/fp_tidy.py +389 -0
  62. ecadlib/fsutil.py +62 -0
  63. ecadlib/house_style.py +1051 -0
  64. ecadlib/httpcache.py +124 -0
  65. ecadlib/hub.py +1490 -0
  66. ecadlib/hub_copy.py +161 -0
  67. ecadlib/hub_tools.py +79 -0
  68. ecadlib/kicad_dblib.py +257 -0
  69. ecadlib/kicad_export.py +837 -0
  70. ecadlib/kicad_import.py +913 -0
  71. ecadlib/kicad_index.py +160 -0
  72. ecadlib/kicad_menu.py +193 -0
  73. ecadlib/kicad_pcm.py +475 -0
  74. ecadlib/launcher.py +156 -0
  75. ecadlib/librarian.py +963 -0
  76. ecadlib/lifecycle.py +691 -0
  77. ecadlib/llm/__init__.py +276 -0
  78. ecadlib/llm/cli.py +70 -0
  79. ecadlib/llm/openai_compat.py +214 -0
  80. ecadlib/llm/profiles.py +245 -0
  81. ecadlib/llm/rails.py +570 -0
  82. ecadlib/llm/run.py +126 -0
  83. ecadlib/llm/sampling.py +49 -0
  84. ecadlib/mcp_server.py +723 -0
  85. ecadlib/messages.py +419 -0
  86. ecadlib/migrate.py +407 -0
  87. ecadlib/notify.py +329 -0
  88. ecadlib/package_schema.json +1238 -0
  89. ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
  90. ecadlib/package_specs/C_0402_1005Metric.json +22 -0
  91. ecadlib/package_specs/C_0805_2012Metric.json +22 -0
  92. ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
  93. ecadlib/package_specs/D_SMA.json +27 -0
  94. ecadlib/package_specs/D_SMB.json +27 -0
  95. ecadlib/package_specs/D_SOD-123.json +43 -0
  96. ecadlib/package_specs/D_SOD-323.json +43 -0
  97. ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
  98. ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
  99. ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
  100. ecadlib/package_specs/L_0805_2012Metric.json +22 -0
  101. ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
  102. ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
  103. ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
  104. ecadlib/package_specs/R_0402_1005Metric.json +22 -0
  105. ecadlib/package_specs/R_0603_1608Metric.json +22 -0
  106. ecadlib/package_specs/R_0805_2012Metric.json +22 -0
  107. ecadlib/package_specs/R_1206_3216Metric.json +22 -0
  108. ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
  109. ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
  110. ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
  111. ecadlib/package_specs/SOT-23-5.json +57 -0
  112. ecadlib/package_specs/SOT-23-6.json +62 -0
  113. ecadlib/package_specs/SOT-23.json +48 -0
  114. ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
  115. ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
  116. ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  117. ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
  118. ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
  119. ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
  120. ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
  121. ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  122. ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  123. ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
  124. ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
  125. ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
  126. ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
  127. ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
  128. ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
  129. ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
  130. ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
  131. ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
  132. ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
  133. ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
  134. ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
  135. ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
  136. ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
  137. ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
  138. ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
  139. ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
  140. ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
  141. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
  142. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
  143. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
  144. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
  145. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
  146. ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
  147. ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
  148. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
  149. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
  150. ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
  151. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
  152. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
  153. ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
  154. ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
  155. ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
  156. ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
  157. ecadlib/package_specs/standard.json.gz +0 -0
  158. ecadlib/parity.py +1204 -0
  159. ecadlib/parts_export.py +154 -0
  160. ecadlib/partview.py +215 -0
  161. ecadlib/plugins.py +231 -0
  162. ecadlib/providers.py +226 -0
  163. ecadlib/rebuild.py +471 -0
  164. ecadlib/release_check.py +437 -0
  165. ecadlib/reports.py +274 -0
  166. ecadlib/resolve.py +556 -0
  167. ecadlib/review_cli.py +292 -0
  168. ecadlib/rules.py +979 -0
  169. ecadlib/rules_infer.py +1205 -0
  170. ecadlib/sexpr.py +149 -0
  171. ecadlib/sheetsafe.py +65 -0
  172. ecadlib/sim_models.py +1087 -0
  173. ecadlib/sourcing.py +1161 -0
  174. ecadlib/stdparts.py +755 -0
  175. ecadlib/step_gen.py +929 -0
  176. ecadlib/symbol_layout.py +798 -0
  177. ecadlib/team.py +558 -0
  178. ecadlib/team_setup.py +288 -0
  179. ecadlib/term.py +420 -0
  180. ecadlib/term_views.py +64 -0
  181. ecadlib/testpack.py +334 -0
  182. ecadlib/ui/__init__.py +1 -0
  183. ecadlib/ui/__main__.py +3 -0
  184. ecadlib/ui/server.py +1029 -0
  185. ecadlib/ui/static/hairline.js +2 -0
  186. ecadlib/ui/static/index.html +2098 -0
  187. ecadlib/ui/static/setup.html +294 -0
  188. ecadlib/upkeep.py +717 -0
  189. ecadlib/upstream.py +573 -0
  190. ecadlib/userconfig.py +257 -0
  191. ecadlib/visual_check.py +1603 -0
  192. ecadlib/ws_status.py +203 -0
  193. ecadlib-0.3.0.dist-info/METADATA +2245 -0
  194. ecadlib-0.3.0.dist-info/RECORD +199 -0
  195. ecadlib-0.3.0.dist-info/WHEEL +5 -0
  196. ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
  197. ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  198. ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
  199. ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/cfb.py ADDED
@@ -0,0 +1,171 @@
1
+ """Minimal Compound File Binary (OLE2 / MS-CFB) *writer*: version 3, 512-byte sectors.
2
+
3
+ write_cfb(path, {"FileHeader": b"...", "Library": {"Header": b"...", "Data": b"..."}})
4
+
5
+ A dict is a storage, bytes is a stream. Streams under the 4096-byte cutoff live in the mini
6
+ stream (64-byte mini sectors); larger ones in regular sectors. FAT/MiniFAT/DIFAT are built to
7
+ fit, so files past 109 FAT sectors (~7 MB) get DIFAT sectors too.
8
+
9
+ Directory: each storage's children form a balanced binary search tree ordered the MS-CFB way
10
+ (UTF-16 length first, then upper-cased code units) with every node black. MS-CFB 2.6.4 allows
11
+ this: "the simplest implementation of the above invariants would be to mark every node as
12
+ black, in which case the tree is simply a binary tree". Timestamps and CLSIDs are left zero.
13
+ olefile (BSD) reads files written here; tests/test_offline.py checks it raises no warnings."""
14
+ import struct
15
+
16
+ SECTOR, MINI, CUTOFF = 512, 64, 4096
17
+ FREESECT, ENDOFCHAIN, FATSECT, DIFSECT, NOSTREAM = 0xFFFFFFFF, 0xFFFFFFFE, 0xFFFFFFFD, 0xFFFFFFFC, 0xFFFFFFFF
18
+ STORAGE, STREAM, ROOT = 1, 2, 5
19
+
20
+
21
+ def _units(name):
22
+ """UTF-16 code units of name (MS-CFB names and their ordering are defined on code units)."""
23
+ b = name.encode("utf-16-le")
24
+ return struct.unpack(f"<{len(b) // 2}H", b)
25
+
26
+
27
+ def _key(name):
28
+ # length first, then code units upper-cased one by one (simple case mapping: a unit whose
29
+ # str.upper() is not a single BMP unit, e.g. 'ß' -> 'SS', is left as is)
30
+ up = []
31
+ for u in _units(name):
32
+ c = chr(u).upper() if not 0xD800 <= u <= 0xDFFF else chr(u)
33
+ up.append(ord(c) if len(c) == 1 and ord(c) <= 0xFFFF else u)
34
+ return (len(up), tuple(up))
35
+
36
+
37
+ class _Entry:
38
+ def __init__(self, name, kind, data=b""):
39
+ if not name or len(_units(name)) > 31 or any(c in name for c in "/\\:!"):
40
+ raise ValueError(f"illegal CFB name {name!r} (1-31 UTF-16 code units, no / \\ : !)")
41
+ self.name, self.kind, self.data = name, kind, data
42
+ self.children = []
43
+ self.left = self.right = self.child = NOSTREAM
44
+ self.start, self.size = ENDOFCHAIN, 0
45
+
46
+
47
+ def _flatten(tree):
48
+ """dict tree -> list of _Entry with sibling/child ids (index 0 = root)."""
49
+ entries = [_Entry("Root Entry", ROOT)]
50
+
51
+ def add(parent, node):
52
+ for name, val in node.items():
53
+ e = _Entry(name, STORAGE if isinstance(val, dict) else STREAM, b"" if isinstance(val, dict) else bytes(val))
54
+ entries.append(e); parent.children.append(len(entries) - 1)
55
+ if isinstance(val, dict):
56
+ add(e, val)
57
+ keys = [_key(entries[i].name) for i in parent.children]
58
+ if len(set(keys)) != len(keys):
59
+ raise ValueError(f"duplicate (case-insensitive) names under {parent.name!r}")
60
+
61
+ def balance(ids): # sorted ids -> root id of a balanced BST
62
+ if not ids:
63
+ return NOSTREAM
64
+ m = len(ids) // 2
65
+ e = entries[ids[m]]
66
+ e.left, e.right = balance(ids[:m]), balance(ids[m + 1:])
67
+ return ids[m]
68
+
69
+ add(entries[0], tree)
70
+ for e in entries:
71
+ if e.kind != STREAM:
72
+ e.child = balance(sorted(e.children, key=lambda i: _key(entries[i].name)))
73
+ return entries
74
+
75
+
76
+ def _chain(fat, first, n):
77
+ for i in range(n):
78
+ fat[first + i] = first + i + 1 if i < n - 1 else ENDOFCHAIN
79
+
80
+
81
+ def to_bytes(tree):
82
+ entries = _flatten(tree)
83
+ streams = [e for e in entries if e.kind == STREAM]
84
+ mini = bytearray(); minifat = []
85
+ for e in streams: # small streams -> mini stream
86
+ e.size = len(e.data)
87
+ if 0 < e.size < CUTOFF:
88
+ n = -(-e.size // MINI)
89
+ e.start = len(minifat); _chain_list(minifat, e.start, n)
90
+ mini += e.data + b"\0" * (n * MINI - e.size)
91
+ root = entries[0]
92
+ root.data, root.size = bytes(mini), len(mini)
93
+
94
+ # sector plan: [mini stream][big streams][minifat][directory][FAT][DIFAT]
95
+ big = ([root] if root.size else []) + [e for e in streams if e.size >= CUTOFF]
96
+ nsec = lambda b: -(-b // SECTOR)
97
+ data_secs = sum(nsec(e.size) for e in big)
98
+ n_minifat = nsec(len(minifat) * 4)
99
+ n_dir = nsec(len(entries) * 128)
100
+ used = data_secs + n_minifat + n_dir
101
+ n_fat = n_difat = 0
102
+ while True: # FAT must also map itself and the DIFAT sectors
103
+ total = used + n_fat + n_difat
104
+ nf = nsec(total * 4)
105
+ nd = 0 if nf <= 109 else -(-(nf - 109) // 127)
106
+ if (nf, nd) == (n_fat, n_difat):
107
+ break
108
+ n_fat, n_difat = nf, nd
109
+ total = used + n_fat + n_difat
110
+ fat = [FREESECT] * (n_fat * SECTOR // 4)
111
+
112
+ pos = 0
113
+ for e in big:
114
+ n = nsec(e.size)
115
+ e.start = pos; _chain(fat, pos, n); pos += n
116
+ minifat_start = pos if n_minifat else ENDOFCHAIN
117
+ if n_minifat:
118
+ _chain(fat, pos, n_minifat); pos += n_minifat
119
+ dir_start = pos; _chain(fat, pos, n_dir); pos += n_dir
120
+ fat_secs = list(range(pos, pos + n_fat))
121
+ for s in fat_secs:
122
+ fat[s] = FATSECT
123
+ pos += n_fat
124
+ difat_secs = list(range(pos, pos + n_difat))
125
+ for s in difat_secs:
126
+ fat[s] = DIFSECT
127
+ assert pos + n_difat == total
128
+
129
+ out = bytearray()
130
+ hdr = bytearray(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" + b"\0" * 16)
131
+ hdr += struct.pack("<HHHHH6sIIIIIIIII", 0x3E, 3, 0xFFFE, 9, 6, b"\0" * 6, 0, n_fat, dir_start, 0, CUTOFF,
132
+ minifat_start, n_minifat, difat_secs[0] if difat_secs else ENDOFCHAIN, n_difat)
133
+ hdr += struct.pack("<109I", *(fat_secs[:109] + [FREESECT] * (109 - min(109, n_fat))))
134
+ out += hdr
135
+
136
+ for e in big:
137
+ out += e.data + b"\0" * (-len(e.data) % SECTOR)
138
+ mf = struct.pack(f"<{len(minifat)}I", *minifat)
139
+ out += mf + b"\xff" * (-len(mf) % SECTOR) # unused MiniFAT slots = FREESECT
140
+ d = b"".join(_dirent(e) for e in entries)
141
+ d += _dirent(None) * ((-len(d) % SECTOR) // 128)
142
+ out += d
143
+ out += struct.pack(f"<{len(fat)}I", *fat)
144
+ rest = fat_secs[109:]
145
+ for i, s in enumerate(difat_secs): # 127 FAT-sector ids + next-DIFAT pointer
146
+ chunk = rest[i * 127:(i + 1) * 127]
147
+ nxt = difat_secs[i + 1] if i + 1 < len(difat_secs) else ENDOFCHAIN
148
+ out += struct.pack("<128I", *(chunk + [FREESECT] * (127 - len(chunk)) + [nxt]))
149
+ assert len(out) == SECTOR * (1 + total), (len(out), total)
150
+ return bytes(out)
151
+
152
+
153
+ def _chain_list(lst, first, n):
154
+ lst.extend(first + i + 1 if i < n - 1 else ENDOFCHAIN for i in range(n))
155
+
156
+
157
+ def _dirent(e):
158
+ if e is None: # unused slot
159
+ return b"\0" * 64 + struct.pack("<HBBIII16sIQQIQ", 0, 0, 0, NOSTREAM, NOSTREAM, NOSTREAM, b"\0" * 16, 0, 0, 0, 0, 0)
160
+ nm = (e.name + "\0").encode("utf-16-le")
161
+ start = 0 if e.kind == STORAGE else (e.start if e.size else ENDOFCHAIN)
162
+ return (nm + b"\0" * (64 - len(nm)) +
163
+ struct.pack("<HBBIII16sIQQIQ", len(nm), e.kind, 1, e.left, e.right, e.child,
164
+ b"\0" * 16, 0, 0, 0, start, e.size))
165
+
166
+
167
+ def write_cfb(path, tree):
168
+ data = to_bytes(tree)
169
+ with open(path, "wb") as f:
170
+ f.write(data)
171
+ return len(data)
ecadlib/cli.py ADDED
@@ -0,0 +1,185 @@
1
+ """One command for everything: ecadlib <command> [options] (or: python -m ecadlib <command>)
2
+
3
+ ecadlib setup the setup page in your browser: AI model + key, Altium / KiCad, folders
4
+ ecadlib start the desktop app (what the Windows Start menu entry runs)
5
+ ecadlib keys list|set NAME|remove NAME API keys stored for you (encrypted on Windows), instead of env variables
6
+ ecadlib init [--yes] [--tool T] [--lib DIR] [--from DIR ...] set up: tool, libraries, workspace, sources -> ecadlib.toml, doctor
7
+ ecadlib demo [-o DIR] a small library from a bundled BOM, offline, no keys (start here)
8
+ ecadlib rules check|show validate the rules file / print the defaults
9
+ ecadlib style learn|show|direct|check|apply|export-dataset learn your house style from your library + your direction
10
+ ecadlib ui --lib <kicad libs> local web UI in your browser (--team: shared server with sign-in)
11
+ ecadlib team add-user|set-role|reset-password|remove|list|setup users and roles for `ecadlib ui --team`; setup: Docker + HTTPS
12
+ ecadlib mcp --lib <kicad libs> MCP server (stdio) for Claude and other agents
13
+ ecadlib triage BOM --lib ... -o out/x BOM triage report (.md + .csv)
14
+ ecadlib footprint gen|compare ... IPC-7351B footprint generator (also BGA, pin headers, connectors, pad tables)
15
+ ecadlib footprint-bench [--offline] compare generated footprints with KiCad's official ones, pad by pad
16
+ ecadlib eval run|compare|sets|show ... accuracy test set: run the pipeline on known parts, pass rate and cost
17
+ ecadlib dblib BOM -o out/altium Altium DbLib (SQLite + XLSX + .DbLib)
18
+ ecadlib altium-lib pcblib|schlib|dump ... write/read Altium .PcbLib/.SchLib files
19
+ ecadlib altium365 plan|workspaces|push ... Altium 365 release via Nexar
20
+ ecadlib dblib-sync plan.json --to DIR [--svn] Altium without 365: sync a plan into a .DbLib/.SVNDbLib folder
21
+ ecadlib librarian BOM --lib ... -o out/lib BOM -> missing parts filled -> Altium models + plan + review
22
+ ecadlib datasheet PDF --part MPN | batch PLAN|BOM | estimate PLAN|BOM Claude reads datasheets -> symbols + footprints
23
+ ecadlib release-check plan.json [--allow P] hold back plan items that fail the pre-release checks
24
+ ecadlib kicad-export plan.json -o DIR --name LIB the same library as KiCad files (.kicad_sym, .pretty)
25
+ ecadlib eagle-export plan.json -o DIR --name LIB the same library for Autodesk Fusion Electronics / EAGLE (.lbr)
26
+ ecadlib easyeda-export plan.json -o DIR --name LIB the same library for EasyEDA Pro (.elibz)
27
+ ecadlib resolve plan.json [--part P] list|show|choose ... fix held parts: ranked candidates, pick one
28
+ ecadlib review [plan.json] [--part P] [--yes] go through held parts one by one: why, pick a candidate, release anyway
29
+ ecadlib upstream check-kicad|check-a365 ... upstream change alerts (KiCad sources, Altium 365 drift)
30
+ ecadlib a365 pull|status|reconcile PLAN --workspace URL [--state F] two-way Altium 365 sync
31
+ ecadlib queue list|show|approve|reject|rebuild|retry [ID] the library agent's review queue
32
+ ecadlib approve plan.json approve|reject|status [--part P] sign-off before altium365 push
33
+ ecadlib dedupe plan.json [--lib ...] [--merge] duplicate parts, footprints and symbols
34
+ ecadlib sourcing BOM [--qty N] [--alternates] price, stock, second sources and alternates per BOM line
35
+ ecadlib project BOARD.PrjPcb [-o bom.csv] [--variant V] BOM straight from an Altium project (.PrjPcb/.SchDoc)
36
+ ecadlib where-used --projects DIR... [--part X] [--plan plan.json] which projects use which library part
37
+ ecadlib step-gen SPEC.json|FOOTPRINT -o x.step package-shaped 3D model (STEP); --check FILE.step validates
38
+ ecadlib upkeep PLAN|BOM [--checks ...] [--apply] [--github issue|pr] weekly checks, only what is new, one issue/PR
39
+ ecadlib sim-models plan.json [--models DIR] [--attach] find, check and attach SPICE / IBIS simulation models
40
+ ecadlib agent-watch [--dry-run] [--source TYPE:REF] library agent: new / changed / removed parts in watched projects and BOMs
41
+ ecadlib agent run [--dry-run]|serve|status|approve ID|reject ID|push library agent: watch, build, prove, PR, push
42
+ ecadlib symbol PINS.csv|PART -o x.SchLib [--preview x.svg] big-chip symbol with pins grouped by function
43
+ ecadlib visual-check PART|x.kicad_mod --datasheet DS.pdf [--pages 3,7-8] Claude compares our drawings with the datasheet
44
+ ecadlib doctor [--workspace W] [--lib DIR] [--json] check Python, packages, API keys, ecadlib.toml, folders
45
+ ecadlib status [--workspace W] [--json] one-screen summary of a workspace: parts, held, sign-off, last runs
46
+ ecadlib completion bash|zsh|fish shell completion script (e.g. ecadlib completion bash > ~/.ecadlib.bash)
47
+ ecadlib plugins list|export NAME PLAN -o DIR installed part-source / exporter plugins (Python entry points)
48
+ ecadlib notify show|test [--event E] [notify] webhooks (Slack, Teams, JSON): targets, send a test message
49
+ ecadlib kicad-dblib plan.json -o DIR --name LIB [--export] KiCad database library (.kicad_dbl + SQLite)
50
+ ecadlib export-parts plan.json --format csv|json|xlsx [-o F] parts table with stable columns (PLM / ERP)
51
+ ecadlib stdparts resolve "SOIC-8 3.9x4.9mm" | coverage BOM | bench standard packages built offline (librarian source)
52
+ ecadlib kicad-pcm plan.json|KICAD_DIR --out DIR [--kicad 7] KiCad Plugin and Content Manager zip (--plugin: action plugin)
53
+ ecadlib altium-pkg plan.json|MODELS_DIR --out DIR Altium .LibPkg (-> IntLib), install script, relative DbLib
54
+ ecadlib altium-menu build --out DIR [--win-dir W] an "ecadlib" menu inside Altium (script project): add part, check BOM, queue
55
+ ecadlib kicad-menu check-bom NETLIST|SCH what the ecadlib window in KiCad runs; Generate BOM hook for the schematic
56
+ ecadlib costs [--model M] [--all] [--md F] [--xlsx F] estimated AI cost per job, for your model or every model
57
+ ecadlib ai show [--model M] [--detect] | rails which model / provider / guide rails the AI features use
58
+ ecadlib migrate [PATH] [--check] [--dry-run] check / upgrade workspace files to this version (backups first)
59
+ ecadlib rebuild LIB_DIR [--check] rebuild a library from library.lock.json offline; report drift
60
+ ecadlib testpack --out DIR small Altium + KiCad test pack (6 parts) + CHECKLIST.md for a person to open
61
+ ecadlib parity plan.json [--kicad DIR] [--altium DIR] | pair A B | survey KiCad and Altium files of every part match
62
+
63
+ `ecadlib <command> -h` shows that command's options. -q / --quiet prints only the outcome, -v / --verbose every
64
+ detail; colour only on a terminal (NO_COLOR=1 or ECADLIB_COLOR=never turns it off). --debug (or ECADLIB_DEBUG=1) shows tracebacks instead of a
65
+ one-line error. ECADLIB_LIB, ECADLIB_WORKSPACE and ECADLIB_CONFIG are defaults for --lib, --workspace and the rules file."""
66
+ import importlib, sys
67
+
68
+ COMMANDS = {
69
+ "setup": ("ecadlib.easy_setup", "open the setup page: AI model and key, Altium / KiCad, folders, add ecadlib to Altium and KiCad"),
70
+ "start": ("ecadlib.launcher", "the desktop app: start ecadlib in the background and open it in the browser (setup page on first run)"),
71
+ "keys": ("ecadlib.userconfig", "API keys stored for this user instead of environment variables: list, set, remove"),
72
+ "init": ("ecadlib.rules_infer", "set up ecadlib: tool, library folders, workspace, sources; learn your conventions -> ecadlib.toml"),
73
+ "demo": ("ecadlib.firstrun:demo_main", "build a small library from a bundled sample BOM, offline, no API keys"),
74
+ "rules": ("ecadlib.rules", "check or show the library rules"),
75
+ "style": ("ecadlib.house_style", "learn your house style from your library and your written direction; new parts follow it"),
76
+ "ui": ("ecadlib.ui.server", "local web UI"),
77
+ "team": ("ecadlib.team", "users and roles for the team web server (ecadlib ui --team)"),
78
+ "mcp": ("ecadlib.mcp_server", "MCP server over stdio"),
79
+ "triage": ("ecadlib.bom_triage", "BOM triage report"),
80
+ "footprint": ("ecadlib.footprint_gen", "IPC-7351B footprint generator"),
81
+ "check": ("ecadlib.footprint_rules", "check .kicad_mod files against the footprint rule book (KLC / IPC-7351)"),
82
+ "footprint-bench": ("ecadlib.footprint_bench", "compare generated footprints (BGA, connectors, ...) with KiCad's official ones"),
83
+ "eval": ("ecadlib.eval.cli", "accuracy test set: run the pipeline on parts with a known answer, score, compare runs"),
84
+ "dblib": ("ecadlib.altium_export", "Altium DbLib export"),
85
+ "altium-lib": ("ecadlib.altium_lib", "Altium .PcbLib/.SchLib writer"),
86
+ "altium365": ("ecadlib.altium365", "Altium 365 release via Nexar"),
87
+ "dblib-sync": ("ecadlib.altium_dblib_sync", "sync a plan into an Altium .DbLib / .SVNDbLib library folder"),
88
+ "librarian": ("ecadlib.librarian", "BOM to library: fill missing parts, plan, release check"),
89
+ "costs": ("ecadlib.costs", "estimated AI cost per job (datasheet read, BOM, library) for your model or every model"),
90
+ "datasheet": ("ecadlib.datasheet", "Claude datasheet reader: one PDF, or a whole plan/BOM as one Message Batch"),
91
+ "release-check": ("ecadlib.release_check", "pre-release checks on an altium365 plan"),
92
+ "kicad-export": ("ecadlib.kicad_export", "librarian plan -> KiCad .kicad_sym / .pretty / .3dshapes"),
93
+ "eagle-export": ("ecadlib.eagle_export", "librarian plan -> Autodesk Fusion Electronics / EAGLE .lbr, read back pad by pad"),
94
+ "easyeda-export": ("ecadlib.easyeda_pro_export", "librarian plan -> EasyEDA Pro device library .elibz, read back pad by pad"),
95
+ "resolve": ("ecadlib.resolve", "fix held parts: candidates for a missing symbol/footprint, choose one"),
96
+ "review": ("ecadlib.review_cli", "go through held parts one at a time: the reason in plain words, pick a candidate, release anyway, open the datasheet"),
97
+ "upstream": ("ecadlib.upstream", "KiCad upstream changes and Altium 365 drift"),
98
+ "a365": ("ecadlib.a365_sync", "two-way Altium 365 sync: pull workspace components into the plan, status, reconcile"),
99
+ "queue": ("ecadlib.agent_queue", "the library agent's review queue: list, show, approve, reject, rebuild"),
100
+ "approve": ("ecadlib.approvals", "approve / reject plan items before release"),
101
+ "dedupe": ("ecadlib.dedupe", "find and merge duplicate parts, footprints and symbols"),
102
+ "sourcing": ("ecadlib.sourcing", "price, stock, second sources and alternates for a BOM"),
103
+ "step-gen": ("ecadlib.step_gen", "package-shaped STEP 3D models from a package spec or footprint name"),
104
+ "upkeep": ("ecadlib.upkeep", "weekly lifecycle / upstream / price / A365 checks; new changes as one issue or PR"),
105
+ "project": ("ecadlib.altium_project", "BOM straight from an Altium project (.PrjPcb / .SchDoc / ActiveBOM)"),
106
+ "hub": ("ecadlib.hub", "your library across tools: every part once, a column per tool, last changed and by whom"),
107
+ "where-used": ("ecadlib.altium_project:where_used_main", "which projects use which library part, and flags"),
108
+ "sim-models": ("ecadlib.sim_models", "find, check and attach SPICE / IBIS simulation models for a plan"),
109
+ "agent-watch": ("ecadlib.agent_watch", "library agent watch step: new / changed / removed parts in watched projects and BOMs"),
110
+ "agent": ("ecadlib.agent", "library agent: watch projects, build + prove parts, batch PRs, push approved parts, upkeep"),
111
+ "visual-check": ("ecadlib.visual_check", "render footprint/symbol + datasheet pages; Claude checks they match"),
112
+ "symbol": ("ecadlib.symbol_layout", "IC symbol with pins grouped by function (power, ports, debug ...), SVG preview"),
113
+ "doctor": ("ecadlib.doctor", "check the setup: Python, packages, API keys (names only), ecadlib.toml, folders"),
114
+ "status": ("ecadlib.ws_status", "one-screen summary of a workspace: parts, held, waiting for sign-off, last runs"),
115
+ "completion": ("ecadlib.completion", "shell completion script for bash, zsh or fish"),
116
+ "plugins": ("ecadlib.plugins", "installed part-source and exporter plugins; run an exporter on a plan"),
117
+ "notify": ("ecadlib.notify", "webhook notifications ([notify]): show targets, send a test message"),
118
+ "kicad-dblib": ("ecadlib.kicad_dblib", "KiCad database library: .kicad_dbl (ODBC) + SQLite parts table from a plan"),
119
+ "export-parts": ("ecadlib.parts_export", "parts table with stable columns as CSV / JSON / XLSX for PLM / ERP imports"),
120
+ "kicad-pcm": ("ecadlib.kicad_pcm", "package the library for KiCad's Plugin and Content Manager (zip); --plugin: the add-part action plugin"),
121
+ "altium-pkg": ("ecadlib.altium_pkg", "Altium hand-off folder: .LibPkg to compile an IntLib, install script project, relative DbLib"),
122
+ "altium-menu": ("ecadlib.altium_menu", "an 'ecadlib' menu inside Altium: DelphiScript project (add part from MPN, check the project BOM, review queue)"),
123
+ "kicad-menu": ("ecadlib.kicad_menu", "what the ecadlib window in KiCad's PCB editor runs (add part, check BOM, fill, queue)"),
124
+ "ai": ("ecadlib.llm.cli", "bring your own model: provider, model, key variable, capability profile, guide rails"),
125
+ "testpack": ("ecadlib.testpack", "write a 6-part Altium + KiCad test pack with a CHECKLIST.md of what to open and what to see"),
126
+ "migrate": ("ecadlib.migrate", "check or upgrade a workspace's files to this ecadlib's formats (backups first)"),
127
+ "rebuild": ("ecadlib.rebuild", "rebuild a library from its library.lock.json offline; --check reports any drift"),
128
+ "parity": ("ecadlib.parity", "read back the KiCad and the Altium library and compare every part (pads, drills, 3D, pins, fields)"),
129
+ "stdparts": ("ecadlib.stdparts", "standard packages and pinouts without Claude: resolve, list, bench vs KiCad, BOM coverage"),
130
+ }
131
+
132
+
133
+ def main(argv=None):
134
+ argv = sys.argv[1:] if argv is None else list(argv)
135
+ from .cliutil import debug_wanted, env_defaults, report
136
+ debug, argv = debug_wanted(argv)
137
+ if not argv or argv[0] in ("-h", "--help", "help"):
138
+ print(__doc__)
139
+ return 0
140
+ if argv[0] in ("-V", "--version"):
141
+ from . import __version__
142
+ print(f"ecadlib {__version__}")
143
+ return 0
144
+ cmd = COMMANDS.get(argv[0])
145
+ if not cmd:
146
+ print(f"ecadlib: unknown command {argv[0]!r}. Commands: {', '.join(COMMANDS)}", file=sys.stderr)
147
+ return 2
148
+ from . import term, userconfig
149
+ userconfig.apply_env() # keys from `ecadlib keys` / the setup page; env vars win
150
+ rest = term.set_from_argv(argv[1:]) # -q / -v for every command; --json: never styled
151
+ args, notes = env_defaults(argv[0], rest) # ECADLIB_LIB / ECADLIB_WORKSPACE when the flag is not given
152
+ from . import firstrun as FR
153
+ args, more = FR.config_defaults(argv[0], args) # then ecadlib.toml [setup] libs / workspace
154
+ notes += more
155
+ bad = FR.preflight(argv[0], args) # a --lib that does not exist, an unwritable -o
156
+ if bad:
157
+ print(f"ecadlib {argv[0]}: {bad[0]}\n hint: {bad[1]}", file=sys.stderr)
158
+ return 2
159
+ if debug and notes:
160
+ print(f"ecadlib {argv[0]}: " + ", ".join(notes), file=sys.stderr)
161
+ sys.argv = [f"ecadlib {argv[0]}"] + args # so argparse usage lines show the right command
162
+ mod, _, fn = cmd[0].partition(":")
163
+ from .rules import RulesError
164
+ try:
165
+ from .clihelp import installed # descriptions + examples in every -h
166
+ with installed(argv[0], cmd[1]):
167
+ return getattr(importlib.import_module(mod), fn or "main")(args)
168
+ except RulesError as e: # a bad ecadlib.toml: say which key, no traceback
169
+ if debug:
170
+ raise
171
+ print(f"ecadlib {argv[0]}: {e}\n hint: `ecadlib doctor` shows the file and line", file=sys.stderr)
172
+ return 2
173
+ except KeyboardInterrupt:
174
+ if debug:
175
+ raise
176
+ print(f"ecadlib {argv[0]}: interrupted", file=sys.stderr)
177
+ return 130
178
+ except Exception as e: # one line + hint; --debug / ECADLIB_DEBUG=1: the traceback
179
+ if debug:
180
+ raise
181
+ return report(argv[0], e, sys.stderr)
182
+
183
+
184
+ if __name__ == "__main__":
185
+ sys.exit(main())
ecadlib/clihelp.py ADDED
@@ -0,0 +1,149 @@
1
+ """`ecadlib <command> -h` polish, applied by ecadlib/cli.py to whatever parser the command builds:
2
+
3
+ - every command and subcommand gets a one-line description (a subcommand without one takes its `help=` text, a
4
+ command without one takes its line in cli.COMMANDS);
5
+ - every command's help ends with a few examples (EXAMPLES below), so a new user can copy a working line.
6
+
7
+ Done from the outside (a wrapper around ArgumentParser.parse_known_args while the command runs), so the commands'
8
+ own modules stay as they are and `python -m ecadlib.<module>` still works. tests/test_firstrun.py walks the whole
9
+ argparse tree and checks every command has a description, examples and the common flag spellings."""
10
+ import argparse, types
11
+
12
+ EXAMPLES = {
13
+ "init": ["ecadlib init # questions: tool, library folders, workspace, sources",
14
+ "ecadlib init --yes --tool kicad --lib ~/kicad/libs --workspace ws",
15
+ "ecadlib init --from ~/altium/libs --yes # also learn naming / parameters from existing libraries"],
16
+ "demo": ["ecadlib demo", "ecadlib demo -o /tmp/try --tool kicad"],
17
+ "kicad-pcm": ["ecadlib kicad-pcm out/librarian/plan.json -o out/pcm --name mylib",
18
+ "ecadlib kicad-pcm out/kicad -o out/pcm --kicad 7", "ecadlib kicad-pcm --plugin -o out/pcm"],
19
+ "altium-menu": ["ecadlib altium-menu build --out out/altium_menu --workspace D:/ecadlib-ws --win-dir D:\\ecadlib-menu",
20
+ "ecadlib altium-menu add-part LM358DR --manufacturer TI --workspace D:/ecadlib-ws --result-file r.ini"],
21
+ "kicad-menu": ["ecadlib kicad-menu run-job JOB --action add-part --project board.kicad_pcb --mpn LM358DR",
22
+ "ecadlib kicad-menu check-bom board.kicad_sch --no-open"],
23
+ "altium-pkg": ["ecadlib altium-pkg out/librarian/plan.json --out out/altium_pkg --name mylib",
24
+ "ecadlib altium-pkg out/altium_models --out out/altium_pkg"],
25
+ "testpack": ["ecadlib testpack --out altium_kicad_testpack"],
26
+ "costs": ["ecadlib costs", "ecadlib costs --model gemini-3.8-flash", "ecadlib costs --all --md docs/costs.md --xlsx costs.xlsx"],
27
+ "ai": ["ecadlib ai show", "ecadlib ai show --model qwen2.5:7b --detect", "ecadlib ai rails"],
28
+ "rules": ["ecadlib rules check", "ecadlib rules check ecadlib.toml", "ecadlib rules show"],
29
+ "style": ["ecadlib style learn --from ~/altium/libs # learn your conventions -> house_style.json",
30
+ "ecadlib style direct \"IC descriptions start with the function in capitals\"",
31
+ "ecadlib style learn --ai --model qwen2.5:7b # a model (here a local one) writes the guide from your direction",
32
+ "ecadlib style check out/librarian/plan.json", "ecadlib style apply out/librarian/plan.json --ai --dry-run",
33
+ "ecadlib style export-dataset -o train.jsonl --eval-share 0.1"],
34
+ "ui": ["ecadlib ui", "ecadlib ui --lib ~/kicad/libs --workspace ws", "ecadlib ui --team --host 0.0.0.0"],
35
+ "team": ["ecadlib team add-user alice --role librarian", "ecadlib team list", "ecadlib team setup --domain lib.example.com"],
36
+ "mcp": ["claude mcp add ecadlib -- ecadlib mcp --lib ~/kicad/libs --workspace ws"],
37
+ "triage": ["ecadlib triage my_bom.csv -o out/triage", "ecadlib triage my_bom.csv --lib ~/kicad/libs --offline --json"],
38
+ "footprint": ["ecadlib footprint gen ecadlib/package_specs/soic8.json -o out/fp",
39
+ "ecadlib footprint compare out/fp/SOIC-8.kicad_mod ref/SOIC-8.kicad_mod"],
40
+ "check": ["ecadlib check out/fp/*.kicad_mod"],
41
+ "footprint-bench": ["ecadlib footprint-bench --offline", "ecadlib footprint-bench --json bench.json"],
42
+ "eval": ["ecadlib eval sets", "ecadlib eval run --set kicad-v1 --limit 5 --offline", "ecadlib eval compare runA.json runB.json",
43
+ "ecadlib eval gate", "ecadlib eval golden --update"],
44
+ "dblib": ["ecadlib dblib my_bom.csv -o out/altium"],
45
+ "altium-lib": ["ecadlib altium-lib pcblib specs/SOIC-8_3.9x4.9mm_P1.27mm.json -o out/altium", "ecadlib altium-lib dump x.PcbLib"],
46
+ "altium365": ["ecadlib altium365 plan my_bom.csv -o out/a365/plan.json", "ecadlib altium365 push out/a365/plan.json --workspace https://acme.365.altium.com --dry-run"],
47
+ "dblib-sync": ["ecadlib dblib-sync out/library/plan.json --to //server/libs/Components --dry-run"],
48
+ "librarian": ["ecadlib librarian my_bom.csv -o ws/library",
49
+ "ecadlib librarian my_bom.csv --lib ~/kicad/libs -o ws/library --json",
50
+ "ecadlib librarian my_bom.csv --offline --sources standard # no network, no API key"],
51
+ "datasheet": ["ecadlib datasheet TPS62133.pdf --part TPS62133RGTR -o out/ds",
52
+ "ecadlib datasheet estimate ws/library/plan.json"],
53
+ "release-check": ["ecadlib release-check ws/library/plan.json", "ecadlib release-check ws/library/plan.json --allow CH340C"],
54
+ "kicad-export": ["ecadlib kicad-export ws/library/plan.json -o ws/kicad --name mylib"],
55
+ "eagle-export": ["ecadlib eagle-export ws/library/plan.json -o ws/fusion --name mylib",
56
+ "ecadlib eagle-export ws/library/plan.json -o ws/fusion --kicad ws/kicad # convert an existing kicad-export"],
57
+ "easyeda-export": ["ecadlib easyeda-export ws/library/plan.json -o ws/easyeda --name mylib",
58
+ "ecadlib easyeda-export ws/library/plan.json -o ws/easyeda --kicad ws/kicad --json"],
59
+ "review": ["ecadlib review # every held part in the workspace, one at a time",
60
+ "ecadlib review ws/library/plan.json --part CH340C", "ecadlib review --workspace ws --yes # print only, never ask"],
61
+ "resolve": ["ecadlib resolve ws/library/plan.json list", "ecadlib resolve ws/library/plan.json --part CH340C show"],
62
+ "upstream": ["ecadlib upstream check-kicad --offline"],
63
+ "a365": ["ecadlib a365 status ws/library/plan.json --workspace https://acme.365.altium.com"],
64
+ "queue": ["ecadlib queue list", "ecadlib queue approve 12 --user alice"],
65
+ "approve": ["ecadlib approve ws/library/plan.json status", "ecadlib approve ws/library/plan.json approve --part CH340C --user alice"],
66
+ "hub": ["ecadlib hub --workspace ws --lib ~/altium/libs", "ecadlib hub --workspace ws --lack kicad --json",
67
+ "ecadlib hub --workspace ws --part AO3401A", "ecadlib hub --workspace ws --tool kicad",
68
+ "ecadlib hub --workspace ws --project boards/sensor/sensor.kicad_pro --copy-to kicad"],
69
+ "dedupe": ["ecadlib dedupe ws/library/plan.json", "ecadlib dedupe ws/library/plan.json --merge"],
70
+ "sourcing": ["ecadlib sourcing my_bom.csv --qty 100 --alternates"],
71
+ "step-gen": ["ecadlib step-gen SOIC-8_3.9x4.9mm_P1.27mm -o out/soic8.step", "ecadlib step-gen --check out/soic8.step"],
72
+ "upkeep": ["ecadlib upkeep ws/library/plan.json", "ecadlib upkeep ws/library/plan.json --github issue"],
73
+ "project": ["ecadlib project board.PrjPcb -o bom.csv", "ecadlib project board.PrjPcb --json"],
74
+ "where-used": ["ecadlib where-used --projects ~/projects --part CH340C"],
75
+ "sim-models": ["ecadlib sim-models ws/library/plan.json --no-net", "ecadlib sim-models ws/library/plan.json --attach"],
76
+ "agent-watch": ["ecadlib agent-watch --dry-run", "ecadlib agent-watch --source bom_file:boards/main_bom.csv --json"],
77
+ "agent": ["ecadlib agent status", "ecadlib agent run --dry-run", "ecadlib agent approve 3 --user alice"],
78
+ "visual-check": ["ecadlib visual-check out/fp/SOIC-8.kicad_mod --datasheet ds.pdf --pages 3 --render-only"],
79
+ "symbol": ["ecadlib symbol pins.csv -o out/MCU.SchLib --preview out/MCU.svg"],
80
+ "setup": ["ecadlib setup", "ecadlib setup --workspace D:/ecadlib --port 8766"],
81
+ "start": ["ecadlib start", "ecadlib start --setup", "ecadlib start --no-window --no-browser"],
82
+ "keys": ["ecadlib keys list", "ecadlib keys set ANTHROPIC_API_KEY", "ecadlib keys remove NEXAR_TOKEN"],
83
+ "doctor": ["ecadlib doctor", "ecadlib doctor --workspace ws --lib ~/kicad/libs --json"],
84
+ "status": ["ecadlib status", "ecadlib status --workspace ws --json"],
85
+ "completion": ["ecadlib completion bash > ~/.local/share/bash-completion/completions/ecadlib",
86
+ "source <(ecadlib completion zsh)"],
87
+ "plugins": ["ecadlib plugins list", "ecadlib plugins export myexporter ws/library/plan.json -o out/x"],
88
+ "notify": ["ecadlib notify show", "ecadlib notify test --event batch_ready"],
89
+ "kicad-dblib": ["ecadlib kicad-dblib ws/library/plan.json -o ws/kicad --name mylib --export"],
90
+ "export-parts": ["ecadlib export-parts ws/library/plan.json --format xlsx -o parts.xlsx"],
91
+ "parity": ["ecadlib parity ws/library/plan.json", "ecadlib parity ws/library/plan.json --kicad ws/kicad --altium ws/altium",
92
+ "ecadlib parity survey --quick"],
93
+ "stdparts": ["ecadlib stdparts resolve \"SOIC-8 3.9x4.9mm\"", "ecadlib stdparts coverage my_bom.csv"],
94
+ "migrate": ["ecadlib migrate ws --check # exit 1 when files need migrating or are from a newer ecadlib",
95
+ "ecadlib migrate ws --dry-run", "ecadlib migrate ws # backups in .ecadlib_backup/"],
96
+ "rebuild": ["ecadlib rebuild ws/library --check # offline; exit 1 on drift",
97
+ "ecadlib rebuild ws/library --check --keep /tmp/rb --json", "ecadlib rebuild ws/library"],
98
+ }
99
+
100
+
101
+ def _fill(parser, fallback=""):
102
+ if getattr(parser, "_ecadlib_polished", False):
103
+ return
104
+ parser._ecadlib_polished = True
105
+ if not (parser.description or "").strip() and fallback:
106
+ parser.description = fallback
107
+ for act in parser._actions:
108
+ if isinstance(act, argparse._SubParsersAction):
109
+ helps = {ca.dest: ca.help for ca in act._choices_actions}
110
+ for name, sub in act.choices.items():
111
+ _fill(sub, helps.get(name) or "")
112
+
113
+
114
+ def polish(parser, cmd, summary=""):
115
+ """Descriptions everywhere under `parser`, and the examples for `cmd` at the end of its help."""
116
+ _fill(parser, summary)
117
+ ex = EXAMPLES.get(cmd)
118
+ if ex and not getattr(parser, "_ecadlib_examples", False):
119
+ parser._ecadlib_examples = True
120
+ orig = parser.format_help
121
+ text = "\nexamples:\n" + "".join(f" {e}\n" for e in ex)
122
+
123
+ def format_help(self):
124
+ h = orig()
125
+ return h if "\nexamples:" in h else h.rstrip("\n") + "\n" + text
126
+ parser.format_help = types.MethodType(format_help, parser)
127
+ return parser
128
+
129
+
130
+ class installed:
131
+ """with installed(cmd, summary): the first parser the command parses with gets polish(); restored on exit."""
132
+ def __init__(self, cmd, summary=""):
133
+ self.cmd, self.summary, self.done = cmd, summary, False
134
+
135
+ def __enter__(self):
136
+ self.orig = argparse.ArgumentParser.parse_known_args
137
+ me = self
138
+
139
+ def parse_known_args(parser, *a, **k):
140
+ if not me.done:
141
+ me.done = True
142
+ polish(parser, me.cmd, me.summary)
143
+ return me.orig(parser, *a, **k)
144
+ argparse.ArgumentParser.parse_known_args = parse_known_args
145
+ return self
146
+
147
+ def __exit__(self, *exc):
148
+ argparse.ArgumentParser.parse_known_args = self.orig
149
+ return False
ecadlib/cliutil.py ADDED
@@ -0,0 +1,119 @@
1
+ """Quality-of-life plumbing for the `ecadlib` command line (used by ecadlib/cli.py):
2
+
3
+ - environment defaults: ECADLIB_LIB (one or more KiCad library folders, separated by the OS path separator, ':' on
4
+ Linux/macOS, ';' on Windows) and ECADLIB_WORKSPACE (workspace folder) fill in --lib / --workspace for the commands
5
+ that take them when the flag is not given. A flag on the command line always wins. ECADLIB_CONFIG (the rules file)
6
+ is handled by ecadlib.rules.find, so every command that searches for ecadlib.toml uses it.
7
+ - friendly errors: an expected error (missing file, bad JSON, bad table, missing package, no permission) becomes one
8
+ line with a hint instead of a traceback; --debug or ECADLIB_DEBUG=1 shows the traceback."""
9
+ import json, os
10
+
11
+ # env var -> (flag, {command: None (any) | (subcommands the flag belongs to, added at the end of argv)}).
12
+ # Only commands where the flag means the same thing: --workspace is a folder here (a365 / upkeep / lifecycle /
13
+ # upstream use --workspace for an Altium 365 URL, so they are not listed). `agent` reads ECADLIB_WORKSPACE itself.
14
+ ENV_FLAGS = {
15
+ "ECADLIB_LIB": ("--lib", {"triage": None, "librarian": None, "resolve": None, "kicad-export": None, "mcp": None,
16
+ "ui": None, "agent-watch": None, "agent": ("run", "serve"), "doctor": None}),
17
+ "ECADLIB_WORKSPACE": ("--workspace", {"mcp": None, "ui": None, "team": None, "queue": None, "agent-watch": None,
18
+ "visual-check": None, "doctor": None, "status": None}),
19
+ }
20
+ DEBUG_ENV = "ECADLIB_DEBUG"
21
+
22
+
23
+ def _has_flag(argv, flag):
24
+ for a in argv:
25
+ if a == "--":
26
+ return False
27
+ if a == flag or a.startswith(flag + "="):
28
+ return True
29
+ return False
30
+
31
+
32
+ def _subcommand(argv):
33
+ """The first positional word: the subcommand. Options before it (agent's --workspace / --rules) take a value."""
34
+ it = iter(argv)
35
+ for a in it:
36
+ if a == "--":
37
+ return None
38
+ if a.startswith("-"):
39
+ if "=" not in a:
40
+ next(it, None)
41
+ continue
42
+ return a
43
+ return None
44
+
45
+
46
+ def env_defaults(cmd, argv, environ=None):
47
+ """argv (the command's own arguments) with --lib / --workspace added from the environment where the command takes
48
+ them and the user did not give them. Returns (new argv, [notes]) - notes name what was added (for --debug)."""
49
+ environ = os.environ if environ is None else environ
50
+ argv, notes = list(argv), []
51
+ if any(a in ("-h", "--help") for a in argv):
52
+ return argv, notes
53
+ for var, (flag, cmds) in ENV_FLAGS.items():
54
+ val = (environ.get(var) or "").strip()
55
+ if not val or cmd not in cmds or _has_flag(argv, flag):
56
+ continue
57
+ values = [v for v in val.split(os.pathsep) if v.strip()] if flag == "--lib" else [val]
58
+ extra = [x for v in values for x in (flag, v.strip())]
59
+ subs = cmds[cmd]
60
+ if subs is None:
61
+ argv = extra + argv # top-level option: before any subcommand
62
+ elif _subcommand(argv) in subs:
63
+ argv = argv + extra # a subcommand's option: after it
64
+ else:
65
+ continue
66
+ notes.append(f"{flag} from ${var}")
67
+ return argv, notes
68
+
69
+
70
+ def debug_wanted(argv, environ=None):
71
+ """(debug?, argv without --debug). --debug may go anywhere before a bare '--'."""
72
+ environ = os.environ if environ is None else environ
73
+ out, debug, rest = [], False, False
74
+ for a in argv:
75
+ if a == "--":
76
+ rest = True
77
+ if a == "--debug" and not rest:
78
+ debug = True
79
+ continue
80
+ out.append(a)
81
+ return debug or (environ.get(DEBUG_ENV, "").strip().lower() not in ("", "0", "false", "no", "off")), out
82
+
83
+
84
+ def friendly(e):
85
+ """(one-line message, hint, exit code) for an exception, or None when it is not one we expected (a bug)."""
86
+ name = getattr(e, "filename", None)
87
+ if isinstance(e, FileNotFoundError):
88
+ return (f"file not found: {name or e}", f"check the path (relative paths start at {os.getcwd()})", 2)
89
+ if isinstance(e, PermissionError):
90
+ return (f"permission denied: {name or e}", "check the file's owner and permissions, or pick another output folder", 2)
91
+ if isinstance(e, IsADirectoryError):
92
+ return (f"expected a file but got a folder: {name or e}", "name the file inside that folder", 2)
93
+ if isinstance(e, NotADirectoryError):
94
+ return (f"expected a folder but got a file: {name or e}", "check the path", 2)
95
+ if isinstance(e, json.JSONDecodeError):
96
+ return (f"not valid JSON: {e.msg} at line {e.lineno} column {e.colno}",
97
+ "is this the right file? plan.json is written by `ecadlib librarian`", 2)
98
+ if isinstance(e, UnicodeDecodeError):
99
+ return (f"file is not readable text ({e.encoding}: {e.reason})", "save the file as UTF-8 (CSV UTF-8 in Excel)", 2)
100
+ if isinstance(e, ModuleNotFoundError) and e.name and not e.name.startswith("ecadlib"):
101
+ pkg = {"PIL": "pillow", "yaml": "pyyaml"}.get(e.name.split(".")[0], e.name.split(".")[0])
102
+ return (f"missing Python package {e.name!r}", f"pip install {pkg} (or run `ecadlib doctor`)", 2)
103
+ if type(e).__module__.startswith("pandas") and type(e).__name__ in ("EmptyDataError", "ParserError"):
104
+ return (f"cannot read the table: {e}", "a BOM needs a header row (CSV, TSV or XLSX)", 2)
105
+ if isinstance(e, OSError):
106
+ return (f"{e.strerror or e}" + (f": {name}" if name else ""), "check the path and the disk", 2)
107
+ return None
108
+
109
+
110
+ def report(cmd, e, out):
111
+ """Print one line (and a hint) for e to `out`; returns the exit code."""
112
+ f = friendly(e)
113
+ if f is None:
114
+ msg, hint, code = (f"internal error ({type(e).__name__}: {e})",
115
+ f"run again with --debug (or {DEBUG_ENV}=1) for the traceback, and please report it", 1)
116
+ else:
117
+ msg, hint, code = f[0], f[1] + "; --debug shows the traceback", f[2]
118
+ print(f"ecadlib {cmd}: {msg}\n hint: {hint}", file=out)
119
+ return code