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.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- 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
|