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/ws_status.py ADDED
@@ -0,0 +1,203 @@
1
+ """`ecadlib status`: one screen about a workspace - parts in the library plans, held, waiting for sign-off, the last
2
+ triage, the last upkeep run and the library agent's queue. Read-only: it never creates the workspace or any file in it,
3
+ and a damaged file is skipped (and named) instead of stopping the summary.
4
+
5
+ ecadlib status [--workspace W] [--json] (W defaults to $ECADLIB_WORKSPACE, then ./ecadlib_workspace)
6
+
7
+ --json prints one object (stable keys, see README "ecadlib status"):
8
+ {"workspace", "exists", "rules", "boms", "plans": [{"plan", "parts", "ready", "held", "awaiting_approval",
9
+ "duplicates", "updated"}], "totals": {...same counts...}, "last_triage": {...} | null,
10
+ "last_upkeep": {...} | null, "agent": {"state", "heartbeat", "last_run", "queue": {state: n}, "waiting_review"},
11
+ "problems": [text]}
12
+ plus "datasheet": {"answers", "parts", "input_tokens", "output_tokens", "usd", "usd_estimated", "escalations",
13
+ "by_model"} once a Claude datasheet read has been paid for in the workspace."""
14
+ import argparse, datetime, json, os, sys
15
+ from pathlib import Path
16
+
17
+ COUNTS = ("ready", "held", "awaiting_approval", "duplicate")
18
+
19
+
20
+ def _iso(ts):
21
+ return datetime.datetime.fromtimestamp(ts, datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
22
+
23
+
24
+ def _json(p, problems):
25
+ try:
26
+ return json.loads(Path(p).read_text(encoding="utf-8"))
27
+ except (OSError, ValueError) as e:
28
+ problems.append(f"{Path(p).as_posix()}: unreadable ({type(e).__name__})")
29
+ return None
30
+
31
+
32
+ def _files(root, pattern):
33
+ """Regular files (no links) matching pattern under root, newest first."""
34
+ out = []
35
+ for p in root.glob(pattern):
36
+ try:
37
+ if p.is_file() and not p.is_symlink():
38
+ out.append((p.stat().st_mtime, p))
39
+ except OSError:
40
+ continue
41
+ return [p for _, p in sorted(out, key=lambda x: x[0], reverse=True)]
42
+
43
+
44
+ def status(root):
45
+ root = Path(root).expanduser().resolve()
46
+ problems = []
47
+ out = {"workspace": str(root), "exists": root.is_dir(), "rules": None, "boms": 0, "plans": [],
48
+ "totals": {"parts": 0, **{("duplicates" if k == "duplicate" else k): 0 for k in COUNTS}},
49
+ "last_triage": None, "last_upkeep": None,
50
+ "agent": {"state": "never run", "heartbeat": None, "last_run": None, "queue": {}, "waiting_review": 0},
51
+ "problems": problems}
52
+ if not out["exists"]:
53
+ return out
54
+ rel = lambda p: Path(p).relative_to(root).as_posix()
55
+ rf = next((root / n for n in ("ecadlib.toml", "ecadlib_rules.json") if (root / n).is_file()), None)
56
+ out["rules"] = rel(rf) if rf else None
57
+ up = root / "uploads"
58
+ out["boms"] = len(_files(up, "*")) + len(_files(up, "*/*")) if up.is_dir() else 0
59
+
60
+ for pf in _files(root, "librarian/*/plan.json"):
61
+ items = _json(pf, problems)
62
+ if not isinstance(items, list):
63
+ if items is not None:
64
+ problems.append(f"{rel(pf)}: not a plan (expected a list of parts)")
65
+ continue
66
+ st = [i.get("status") for i in items if isinstance(i, dict)]
67
+ row = {"plan": rel(pf), "parts": len(st), **{("duplicates" if k == "duplicate" else k): st.count(k) for k in COUNTS},
68
+ "updated": _iso(pf.stat().st_mtime)}
69
+ out["plans"].append(row)
70
+ for k in out["totals"]:
71
+ out["totals"][k] += row[k]
72
+
73
+ tri = _files(root, "reports/triage_*.json") or _files(root, "reports/triage_*.csv")
74
+ if tri:
75
+ f = tri[0]
76
+ t = {"report": rel(f), "finished": _iso(f.stat().st_mtime)}
77
+ if f.suffix == ".json":
78
+ d = _json(f, problems) or {}
79
+ if isinstance(d, dict):
80
+ t.update(name=d.get("name"), finished=d.get("finished") or t["finished"], user=d.get("user"),
81
+ lines=len(d.get("rows") or []), counts=d.get("counts") or {})
82
+ out["last_triage"] = t
83
+
84
+ ups = _files(root, "upkeep/*/state/upkeep_last.json") + _files(root, "agent/upkeep/*/state/upkeep_last.json")
85
+ best = None
86
+ for f in ups:
87
+ d = _json(f, problems)
88
+ if isinstance(d, dict) and (best is None or str(d.get("date") or "") > str(best["date"] or "")):
89
+ best = {"date": d.get("date"), "new_changes": d.get("new_changes"), "state": rel(f)}
90
+ out["last_upkeep"] = best
91
+
92
+ from . import ds_route # what the Claude datasheet reads cost (<out>/datasheet/cost.jsonl files)
93
+ recs = [r for f in _files(root, "**/datasheet/cost.jsonl") for r in ds_route.read_ledger(f, problems)]
94
+ if recs:
95
+ d = ds_route.summarize(recs)
96
+ out["datasheet"] = {k: d[k] for k in ("answers", "parts", "input_tokens", "output_tokens", "usd", "usd_estimated",
97
+ "escalations")}
98
+ out["datasheet"]["by_model"] = d["by_model"]
99
+ ag = out["agent"]
100
+ sf = root / "agent" / "status.json"
101
+ if sf.is_file():
102
+ d = _json(sf, problems)
103
+ if isinstance(d, dict):
104
+ ag.update(state=d.get("state") or "unknown", heartbeat=d.get("heartbeat"), last_run=d.get("last_run"))
105
+ if d.get("last_error"):
106
+ ag["last_error"] = d["last_error"]
107
+ for f in _files(root, "agent/queue/items/*.json"):
108
+ d = _json(f, problems)
109
+ if isinstance(d, dict) and d.get("status"):
110
+ ag["queue"][d["status"]] = ag["queue"].get(d["status"], 0) + 1
111
+ ag["waiting_review"] = ag["queue"].get("review", 0)
112
+ return out
113
+
114
+
115
+ def render(s, styled=False, width=None, shown=None, verbosity=0):
116
+ """The one-screen summary as text. styled: colour + glyphs (a terminal); shown: the workspace as the user typed it
117
+ (the absolute path only with -v)."""
118
+ import io
119
+ from . import messages as M, term as TM
120
+ buf = io.StringIO()
121
+ t = TM.Term(buf, styled=styled, width=width, verbosity=verbosity)
122
+ name = shown or s["workspace"]
123
+ if not s["exists"]:
124
+ t.write(t.bold(f"Workspace {name} does not exist yet."))
125
+ t.write(" fix: `ecadlib demo -o <folder>` makes a sample one; `ecadlib ui --workspace ...` or "
126
+ "`ecadlib librarian BOM -o <folder>/librarian/x` starts a real one")
127
+ return buf.getvalue().rstrip("\n")
128
+ tot = s["totals"]
129
+ cnt = {"ready": tot["ready"], "held": tot["held"], "awaiting_approval": tot["awaiting_approval"], "duplicate": tot["duplicates"]}
130
+ t.write(t.bold(f"Workspace {name}") + (t.dim(f" ({s['workspace']})") if t.verbose and shown else ""))
131
+ head = (f"{M.plural(tot['parts'], 'part')} in {M.plural(len(s['plans']), 'library plan')}: {M.counts(cnt)}"
132
+ if s["plans"] else "no library yet (no BOM has been through the librarian)")
133
+ t.write(f" {head}")
134
+ if t.quiet:
135
+ return buf.getvalue().rstrip("\n")
136
+ rows = []
137
+ for p in s["plans"][:8]:
138
+ cells = [p["plan"].removesuffix("/plan.json"), M.plural(p["parts"], "part")]
139
+ cells.append(" ".join(t.status(k, f"{p[k]} {M.status_word(k)}") for k in ("ready", "held", "awaiting_approval") if p[k]))
140
+ rows.append(cells)
141
+ if rows:
142
+ t.write("")
143
+ t.write("library plans:")
144
+ t.table(rows, right=(1,))
145
+ if len(s["plans"]) > 8:
146
+ t.write(f" ... {len(s['plans']) - 8} more (--json lists all)")
147
+ t.write("")
148
+ lt, lu, ag = s["last_triage"], s["last_upkeep"], s["agent"]
149
+ kv = [["rules", s["rules"] or "defaults (no ecadlib.toml in the workspace)"],
150
+ ["BOMs", f"{s['boms']} uploaded"],
151
+ ["last triage", (f"{lt.get('name') or lt['report']}, {lt['finished']}"
152
+ + (f", {M.plural(lt['lines'], 'line')}" if lt.get("lines") is not None else "")
153
+ + (f" ({', '.join(f'{k} {v}' for k, v in lt['counts'].items())})" if lt.get("counts") else "")
154
+ if lt else "never")],
155
+ ["upkeep", f"{lu['date']}, {M.plural(lu['new_changes'] or 0, 'new change')}" if lu else "never"]]
156
+ ds = s.get("datasheet")
157
+ if ds:
158
+ kv.append(["datasheet", f"{ds['answers']} Claude answers for {M.plural(ds['parts'], 'part')}, ${ds['usd']:.2f}"
159
+ + (f" (+ ${ds['usd_estimated']:.2f} estimated)" if ds.get("usd_estimated") else "")
160
+ + f", {ds['input_tokens']:,} tokens in: "
161
+ + ", ".join(f"{m} {v['answers']} (${v['usd']:.2f})" for m, v in sorted(ds["by_model"].items()))])
162
+ q = ", ".join(f"{v} {M.status_word(k)}" for k, v in sorted(ag["queue"].items())) or "empty"
163
+ kv.append(["agent", f"{ag['state']}" + (f", last heartbeat {ag['heartbeat']}" if ag.get("heartbeat") else "")
164
+ + f"; review queue: {q}" + (f"; {ag['waiting_review']} waiting for review" if ag["waiting_review"] else "")])
165
+ if ag.get("last_error"):
166
+ kv.append(["", t.color("last error: ", "red") + M.clean(str(ag["last_error"]))[:160]])
167
+ t.table(kv, indent=0)
168
+ for p in s["problems"]:
169
+ t.note(f"WARN {M.clean(p)}", "the file was skipped; `ecadlib migrate --check` or `ecadlib doctor` says more")
170
+ nxt = []
171
+ ws = f" --workspace {shown}" if shown else ""
172
+ if tot["held"]:
173
+ nxt.append((f"ecadlib review{ws}", f"{M.plural(tot['held'], 'held part')}: why, and pick a fix (or `ecadlib resolve <plan.json> list`)"))
174
+ if tot["awaiting_approval"]:
175
+ nxt.append(("ecadlib approve <plan.json> status", "who still has to sign off"))
176
+ if ag["waiting_review"]:
177
+ nxt.append((f"ecadlib queue list{ws}", "the agent's review queue"))
178
+ if not s["plans"]:
179
+ nxt.append((f"ecadlib librarian BOM.csv -o {shown or '<workspace>'}/librarian/board", "fill a library from a BOM"))
180
+ if nxt:
181
+ t.write("")
182
+ t.write("next:")
183
+ t.table([[c, f"# {w}"] for c, w in nxt[:3]])
184
+ return buf.getvalue().rstrip("\n")
185
+
186
+
187
+ def main(argv=None):
188
+ ap = argparse.ArgumentParser(prog="ecadlib status", description="one-screen summary of a workspace (read-only)")
189
+ ap.add_argument("--workspace", default=os.environ.get("ECADLIB_WORKSPACE") or "ecadlib_workspace")
190
+ ap.add_argument("--json", action="store_true", help="print the summary as JSON (stable keys; see README)")
191
+ a = ap.parse_args(argv)
192
+ s = status(a.workspace)
193
+ if a.json:
194
+ print(json.dumps(s, indent=2))
195
+ else:
196
+ from . import term as TM
197
+ print(render(s, styled=TM.wants_style(sys.stdout), width=TM.term_width(sys.stdout), shown=a.workspace,
198
+ verbosity=TM.VERBOSITY))
199
+ return 0 if s["exists"] else 1
200
+
201
+
202
+ if __name__ == "__main__":
203
+ sys.exit(main())