@arnilo/prism-tools 0.0.0-stage → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/CHANGELOG.md +410 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +28 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +545 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ """Render every slide of a .pptx to per-slide PNG images.
3
+
4
+ Pipeline: LibreOffice (soffice --headless --convert-to pdf) turns the deck
5
+ into a PDF, then poppler (pdftoppm, or pdftocairo as an alternate) splits
6
+ the PDF into one PNG per slide.
7
+
8
+ Output is JSON. When both tools are present:
9
+ {"rendered": true, "files": ["render/slide-1.png", ...]}
10
+ When either tool is missing the script still exits 0 and reports:
11
+ {"rendered": false, "missing": ["soffice"], "guidance": "..."}
12
+ so callers can degrade gracefully (fall back to pptx_read.py --outline).
13
+ """
14
+ import argparse
15
+ import glob
16
+ import json
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+
23
+
24
+ def find_tools():
25
+ """Return (soffice, splitter, missing) using shutil.which."""
26
+ soffice = shutil.which("soffice")
27
+ splitter = shutil.which("pdftoppm") or shutil.which("pdftocairo")
28
+ missing = []
29
+ if not soffice:
30
+ missing.append("soffice")
31
+ if not splitter:
32
+ missing.append("pdftoppm (or pdftocairo)")
33
+ return soffice, splitter, missing
34
+
35
+
36
+ def render(pptx_path, out_dir, prefix, dpi):
37
+ soffice, splitter, missing = find_tools()
38
+ if missing:
39
+ return {
40
+ "rendered": False, "missing": missing,
41
+ "guidance": "Install LibreOffice (soffice) and poppler-utils "
42
+ "(pdftoppm/pdftocairo) to render slides. Without "
43
+ "them, verify decks with pptx_read.py --outline "
44
+ "instead.",
45
+ }
46
+
47
+ os.makedirs(out_dir, exist_ok=True)
48
+ with tempfile.TemporaryDirectory() as tmp:
49
+ proc = subprocess.run(
50
+ [soffice, "--headless", "--convert-to", "pdf",
51
+ "--outdir", tmp, pptx_path],
52
+ capture_output=True, text=True, encoding="utf-8",
53
+ errors="replace", timeout=300)
54
+ pdfs = glob.glob(os.path.join(tmp, "*.pdf"))
55
+ if proc.returncode != 0 or not pdfs:
56
+ raise SystemExit(f"soffice PDF conversion failed: {proc.stderr}")
57
+ pdf = pdfs[0]
58
+ out_prefix = os.path.join(out_dir, prefix)
59
+ proc = subprocess.run(
60
+ [splitter, "-png", "-r", str(dpi), pdf, out_prefix],
61
+ capture_output=True, text=True, encoding="utf-8",
62
+ errors="replace", timeout=300)
63
+ if proc.returncode != 0:
64
+ raise SystemExit(f"{os.path.basename(splitter)} failed: "
65
+ f"{proc.stderr}")
66
+
67
+ files = sorted(glob.glob(out_prefix + "*.png"))
68
+ return {"rendered": True, "files": files}
69
+
70
+
71
+ def main(argv=None):
72
+ if hasattr(sys.stdout, "reconfigure"):
73
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
74
+ parser = argparse.ArgumentParser(
75
+ description="Render each slide of a .pptx to a PNG via "
76
+ "soffice + pdftoppm/pdftocairo.",
77
+ epilog=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
78
+ parser.add_argument("pptx", help="path to the .pptx file")
79
+ parser.add_argument("--outdir", default="render",
80
+ help="directory for PNGs (default: ./render)")
81
+ parser.add_argument("--prefix", default="slide",
82
+ help="PNG filename prefix (default: slide)")
83
+ parser.add_argument("--dpi", type=int, default=100,
84
+ help="render resolution (default: 100)")
85
+ args = parser.parse_args(argv)
86
+
87
+ result = render(args.pptx, args.outdir, args.prefix, args.dpi)
88
+ print(json.dumps(result, indent=2))
89
+ return 0
90
+
91
+
92
+ if __name__ == "__main__":
93
+ sys.exit(main())
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nous Research
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: xlsx
3
+ description: Create, read, edit Excel .xlsx workbooks and CSVs.
4
+ version: 1.1.0
5
+ author: Nous Research
6
+ license: MIT
7
+ platforms: [linux, macos, windows]
8
+ metadata:
9
+ hermes:
10
+ tags: [excel, spreadsheet, xlsx, csv, openpyxl, productivity]
11
+ category: productivity
12
+ related_skills: [docx, pdf, powerpoint]
13
+ ---
14
+
15
+ # Xlsx Skill
16
+
17
+ Work with Excel .xlsx workbooks using Python and openpyxl: build styled
18
+ multi-sheet workbooks with formulas and charts, inspect or dump existing
19
+ files, edit cells and structure, and convert to/from CSV. All helper
20
+ scripts are argparse CLIs that print JSON and use explicit UTF-8 I/O.
21
+
22
+ ## When to Use
23
+
24
+ - Creating .xlsx reports: multiple sheets, number formats, styling,
25
+ merged cells, freeze panes, autofilter, conditional formatting,
26
+ charts, data-validation dropdowns, native Excel tables, defined
27
+ names, hyperlinks, cell notes, sheet protection.
28
+ - Reading a workbook: sheet inventory, dumping data as JSON or CSV,
29
+ listing formulas vs cached values, notes, defined names, tables.
30
+ - Editing existing files: set cells, append rows, insert/delete
31
+ rows/columns (reference-aware via `xlsx_restructure.py`),
32
+ copy/rename sheets, tables, names, notes, protection.
33
+ - Recalculating formulas headlessly via LibreOffice
34
+ (`xlsx_recalc.py`).
35
+ - CSV interop with type inference and non-UTF-8 encodings.
36
+ - Not for the legacy .xls binary format (use LibreOffice to convert
37
+ first: `soffice --headless --convert-to xlsx old.xls`).
38
+
39
+ ## Prerequisites
40
+
41
+ - Python 3.10+ with `openpyxl` (`pip install openpyxl`). No other
42
+ third-party packages are needed; everything else is stdlib.
43
+ - Optional: LibreOffice (`soffice`) for headless recalculation or
44
+ format conversion.
45
+
46
+ ## How to Run
47
+
48
+ Run the helper scripts with the `terminal` tool from this skill's
49
+ `scripts/` directory (every script supports `--help`):
50
+
51
+ ```bash
52
+ python scripts/xlsx_create.py spec.json report.xlsx # build from JSON spec
53
+ python scripts/xlsx_read.py report.xlsx --sheets # inventory
54
+ python scripts/xlsx_read.py report.xlsx --json --sheet Data
55
+ python scripts/xlsx_read.py report.xlsx --formulas
56
+ python scripts/xlsx_edit.py report.xlsx --sheet Data --set B2=42 --recalc
57
+ python scripts/xlsx_restructure.py report.xlsx --sheet Data --insert-rows 3:2
58
+ python scripts/xlsx_recalc.py report.xlsx
59
+ python scripts/csv_to_xlsx.py data.csv out.xlsx --encoding utf-8
60
+ python scripts/xlsx_to_csv.py report.xlsx out.csv --sheet Data
61
+ ```
62
+
63
+ Author the JSON spec with `write_file`, inspect script JSON output with
64
+ `read_file` or directly from stdout.
65
+
66
+ ## Quick Reference
67
+
68
+ | Task | Command |
69
+ |---|---|
70
+ | Create workbook from spec | `xlsx_create.py spec.json out.xlsx` |
71
+ | Sheet names + dimensions | `xlsx_read.py f.xlsx --sheets` |
72
+ | Dump sheet as JSON | `xlsx_read.py f.xlsx --json --sheet S` |
73
+ | Dump sheet as CSV | `xlsx_read.py f.xlsx --csv --out d.csv` |
74
+ | List formulas + cached values | `xlsx_read.py f.xlsx --formulas` |
75
+ | Set a cell / formula | `xlsx_edit.py f.xlsx --set "A1==SUM(B:B)"` |
76
+ | Append a row | `xlsx_edit.py f.xlsx --append '[1,"x",true]'` |
77
+ | Insert 2 rows, refs NOT shifted | `xlsx_edit.py f.xlsx --insert-rows 3:2` |
78
+ | Insert 2 rows, refs shifted | `xlsx_restructure.py f.xlsx --insert-rows 3:2` |
79
+ | Delete a column, refs shifted | `xlsx_restructure.py f.xlsx --delete-cols B` |
80
+ | Create a native table | `xlsx_edit.py f.xlsx --add-table Sales:A1:C9` |
81
+ | Append inside a table | `--table-append 'Sales=["West",5]'` |
82
+ | List tables | `xlsx_edit.py f.xlsx --list-tables` |
83
+ | Defined names | `--define-name "Rates='Data'!$B$2:$B$9"` / `--delete-name Rates` / `xlsx_read.py f.xlsx --names` |
84
+ | Hyperlink | `--hyperlink "A1=https://example.com|Docs"` |
85
+ | Cell note | `--note "B2=Check this|Reviewer"`; read via `xlsx_read.py f.xlsx --notes` |
86
+ | Protect sheet (see Pitfalls) | `--protect your-password --unlock B2:B9` |
87
+ | Recalculate via LibreOffice | `xlsx_recalc.py f.xlsx` |
88
+ | Copy / rename sheet | `--copy-sheet Src:New --rename-sheet Old:New` |
89
+ | Force recalc on open | `xlsx_edit.py f.xlsx --recalc` |
90
+ | CSV -> styled xlsx | `csv_to_xlsx.py in.csv out.xlsx` |
91
+ | xlsx -> CSV | `xlsx_to_csv.py f.xlsx out.csv --encoding utf-8` |
92
+
93
+ ## Procedure
94
+
95
+ 1. **Create**: write a JSON spec (schema documented in
96
+ `xlsx_create.py --help` and its docstring). Each sheet supports
97
+ `rows` (scalars or styled cell objects), sparse `cells` overrides,
98
+ `column_widths`, `row_heights`, `merges`, `freeze_panes`,
99
+ `autofilter`, `conditional_formats` (cell_is rules and color
100
+ scales), `charts` (bar/line/pie from cell ranges),
101
+ `validations` (list dropdowns), `tables` (native Excel tables with
102
+ a style name), and `protection`. Workbook-level `defined_names`
103
+ maps names to refs. Cell objects also take `hyperlink` and `note`.
104
+ Typed values: JSON numbers/bools
105
+ pass through; dates use `{"value": "2026-01-31", "type": "date"}`.
106
+ Number formats are Excel format strings: currency `"$#,##0.00"`,
107
+ percent `"0.0%"`, date `"yyyy-mm-dd"`.
108
+ 2. **Formulas**: set with `"formula": "SUM(B2:B9)"` in the spec or
109
+ `--set "C1==SUM(A:A)"` in the editor. When writing formulas, add
110
+ `"full_calc_on_load": true` (spec) or `--recalc` (editor); this sets
111
+ the workbook's `fullCalcOnLoad` flag so Excel/LibreOffice recompute
112
+ everything on open. openpyxl itself NEVER evaluates formulas.
113
+ 3. **Read**: `--sheets` for inventory (names, dimensions, merged
114
+ ranges, chart count, tables, protection, defined names),
115
+ `--json`/`--csv` for data, `--formulas` to
116
+ pair each formula string with its cached result, `--notes` for
117
+ cell comments, `--names` for defined names. Cached results
118
+ exist only if the file was last saved by a real spreadsheet app;
119
+ files fresh from openpyxl return `null` there. To materialize
120
+ results headlessly run `xlsx_recalc.py file.xlsx` (uses
121
+ LibreOffice; prints `{"recalculated": false, ...}` and exits 0
122
+ when `soffice` is absent), then reload with `--data-only`.
123
+ 4. **Edit**: `xlsx_edit.py` applies renames/copies first, then
124
+ structural row/column changes, then `--set`/`--append`. It edits in
125
+ place unless `--out` is given — copy the file first if you need the
126
+ original.
127
+ 5. **Restructure**: for insert/delete on sheets that have formulas,
128
+ merges, tables, or filters, use `xlsx_restructure.py` instead of
129
+ `xlsx_edit.py`. It rewrites formula references on ALL sheets
130
+ (absolute `$` refs, ranges, cross-sheet refs), shifts merges,
131
+ autofilter, freeze panes, validation and conditional-format
132
+ ranges, table refs, defined names, and row/column dimensions, then
133
+ prints a JSON report including a `not_shifted` list. Rules and
134
+ limits: `references/restructuring.md`.
135
+ 6. **CSV interop**: `csv_to_xlsx.py` infers int/float/bool/ISO-date
136
+ per cell and styles the header row; `xlsx_to_csv.py` writes ISO
137
+ dates and blank strings for empty cells. Both default to UTF-8 and
138
+ accept `--encoding` (e.g. `utf-8-sig` for Excel-friendly BOM,
139
+ `cp1252` for legacy Windows exports).
140
+
141
+ ## Converting to PDF
142
+
143
+ LibreOffice converts headlessly (also works for CSV export of a single
144
+ sheet):
145
+
146
+ ```bash
147
+ soffice --headless --convert-to pdf report.xlsx --outdir out/
148
+ soffice --headless --convert-to csv report.xlsx --outdir out/ # 1st sheet only
149
+ ```
150
+
151
+ Only the first sheet lands in a CSV; for other sheets use
152
+ `xlsx_to_csv.py --sheet NAME`. If `soffice` is missing, install
153
+ LibreOffice or hand the file to the user unconverted.
154
+
155
+ ## Pitfalls
156
+
157
+ - **openpyxl does not calculate.** Formula results are available only
158
+ via `load_workbook(path, data_only=True)` and only when the file was
159
+ previously saved by Excel/LibreOffice. Otherwise you get `None`.
160
+ - **`xlsx_edit.py` insert/delete does not shift references** (raw
161
+ openpyxl behavior). Use `xlsx_restructure.py`, which does — but even
162
+ it cannot move chart anchors, images, or conditional-format RULE
163
+ formulas; read its JSON report's `not_shifted` list and
164
+ `references/restructuring.md`.
165
+ - **Sheet protection is NOT security.** `--protect` sets the standard
166
+ xlsx sheet-protection hash: it signals "don't edit this" to
167
+ well-behaved apps and nothing more. Anyone can strip it by editing
168
+ the zip's XML or unchecking it in LibreOffice. Never rely on it for
169
+ confidentiality or integrity; it does not encrypt anything.
170
+ - **`data_only=True` then save** silently discards all formulas
171
+ (cached values replace them). Never save a workbook loaded that way
172
+ unless that is the goal.
173
+ - **Loading strips charts/images**: openpyxl does not round-trip
174
+ charts, so editing a charted workbook and saving drops the charts.
175
+ Re-add charts after editing, or avoid re-saving charted files.
176
+ - **CSV locale traps**: always pass explicit encodings (the scripts
177
+ already do) and remember European CSVs often use `;` delimiters and
178
+ decimal commas — use `--delimiter ';'` and expect strings like
179
+ `"12,5"` to stay strings.
180
+ - **Dates are datetimes**: Excel stores dates as serial numbers;
181
+ openpyxl returns `datetime`/`date` objects. Dumps here emit ISO
182
+ strings.
183
+ - Sheet names are capped at 31 chars and reject `[ ] : * ? / \`.
184
+
185
+ ## Verification
186
+
187
+ - After creating: `xlsx_read.py out.xlsx --sheets` and confirm sheet
188
+ names, dimensions, merged ranges, and chart counts match intent.
189
+ - Dump data with `--json` and compare against the source values.
190
+ - After edits: re-dump the touched range; if formulas were written,
191
+ confirm `--formulas` lists them and that `--recalc` was applied.
192
+ - After `xlsx_restructure.py`: read its JSON report, then re-run
193
+ `--formulas` and `--sheets` to confirm references and ranges landed
194
+ where expected.
195
+ - For a full visual check, open in LibreOffice:
196
+ `soffice --headless --convert-to pdf out.xlsx` and inspect the PDF.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """Convert a CSV file to a styled .xlsx workbook with type inference.
3
+
4
+ Type inference per cell (disable with --no-infer):
5
+ int, float, bool ("true"/"false", case-insensitive), ISO date
6
+ (YYYY-MM-DD) and ISO datetime; everything else stays a string.
7
+
8
+ Styling applied by default (disable with --plain):
9
+ bold header row with a light fill, frozen top row, autofilter over the
10
+ data range, and column widths sized to the longest cell (capped at 60).
11
+
12
+ Usage:
13
+ csv_to_xlsx.py data.csv out.xlsx
14
+ csv_to_xlsx.py data.csv out.xlsx --sheet-name Import --encoding cp1252
15
+ csv_to_xlsx.py data.csv out.xlsx --delimiter ';' --no-infer
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import csv
21
+ import json
22
+ import sys
23
+ from datetime import date, datetime
24
+
25
+ from openpyxl import Workbook
26
+ from openpyxl.styles import Font, PatternFill
27
+ from openpyxl.utils import get_column_letter
28
+
29
+ MAX_COL_WIDTH = 60
30
+ COL_PADDING = 2
31
+ DEFAULT_COL_WIDTH = 8
32
+
33
+
34
+ def infer(text):
35
+ if text == "":
36
+ return None
37
+ low = text.lower()
38
+ if low in ("true", "false"):
39
+ return low == "true"
40
+ for caster in (int, float):
41
+ try:
42
+ return caster(text)
43
+ except ValueError:
44
+ pass
45
+ for parser in (date.fromisoformat, datetime.fromisoformat):
46
+ try:
47
+ return parser(text)
48
+ except ValueError:
49
+ pass
50
+ return text
51
+
52
+
53
+ def main(argv=None):
54
+ ap = argparse.ArgumentParser(description="CSV -> styled .xlsx converter.")
55
+ ap.add_argument("csv_file", help="input CSV path")
56
+ ap.add_argument("output", help="output .xlsx path")
57
+ ap.add_argument("--sheet-name", default="Sheet1")
58
+ ap.add_argument("--encoding", default="utf-8",
59
+ help="CSV file encoding (default utf-8)")
60
+ ap.add_argument("--delimiter", default=",")
61
+ ap.add_argument("--no-infer", action="store_true",
62
+ help="keep every cell as a string")
63
+ ap.add_argument("--plain", action="store_true",
64
+ help="skip header styling / freeze / autofilter")
65
+ args = ap.parse_args(argv)
66
+
67
+ with open(args.csv_file, newline="", encoding=args.encoding) as fh:
68
+ rows = list(csv.reader(fh, delimiter=args.delimiter))
69
+
70
+ wb = Workbook()
71
+ ws = wb.active
72
+ ws.title = args.sheet_name
73
+ for i, row in enumerate(rows):
74
+ if args.no_infer or i == 0:
75
+ ws.append(row)
76
+ else:
77
+ ws.append([infer(cell) for cell in row])
78
+
79
+ if rows and not args.plain:
80
+ header_font = Font(bold=True)
81
+ header_fill = PatternFill("solid", fgColor="DDEBF7")
82
+ for cell in ws[1]:
83
+ cell.font = header_font
84
+ cell.fill = header_fill
85
+ ws.freeze_panes = "A2"
86
+ ws.auto_filter.ref = ws.dimensions
87
+ for col_idx in range(1, ws.max_column + 1):
88
+ longest = max((len(str(r[col_idx - 1])) for r in rows
89
+ if len(r) >= col_idx), default=DEFAULT_COL_WIDTH)
90
+ ws.column_dimensions[get_column_letter(col_idx)].width = \
91
+ min(longest + COL_PADDING, MAX_COL_WIDTH)
92
+
93
+ wb.save(args.output)
94
+ print(json.dumps({"ok": True, "output": args.output,
95
+ "rows": len(rows)}, ensure_ascii=False))
96
+ return 0
97
+
98
+
99
+ if __name__ == "__main__":
100
+ try:
101
+ sys.exit(main())
102
+ except Exception as exc: # noqa: BLE001
103
+ print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
104
+ sys.exit(1)
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python3
2
+ """Create an .xlsx workbook from a JSON spec.
3
+
4
+ Spec (JSON object):
5
+ {
6
+ "full_calc_on_load": true, # force recalculation on open (optional)
7
+ "defined_names": {"Rates": "'Data'!$B$2:$B$4"}, # workbook scope
8
+ "sheets": [
9
+ {
10
+ "name": "Data",
11
+ "rows": [["Header", 1, true], ...], # scalars or cell objects (see below)
12
+ "cells": {"A1": {"value": 5, "format": "0.00%"}}, # sparse overrides
13
+ "column_widths": {"A": 22, "B": 12},
14
+ "row_heights": {"1": 24},
15
+ "merges": ["A1:C1"],
16
+ "freeze_panes": "A2",
17
+ "autofilter": "A1:C10",
18
+ "conditional_formats": [
19
+ {"range": "B2:B9", "type": "cell_is", "operator": "greaterThan",
20
+ "formula": ["100"], "fill": "FFC7CE"},
21
+ {"range": "C2:C9", "type": "color_scale"}
22
+ ],
23
+ "charts": [
24
+ {"type": "bar", "title": "Sales", "anchor": "F2",
25
+ "data": "B1:B5", "categories": "A2:A5"}
26
+ ],
27
+ "validations": [
28
+ {"range": "D2:D9", "type": "list", "formula1": "\"Yes,No,Maybe\""}
29
+ ],
30
+ "tables": [
31
+ {"name": "Sales", "range": "A1:C4",
32
+ "style": "TableStyleMedium9"} # native Excel table
33
+ ],
34
+ "protection": {"password": "your-password", # NOT security --
35
+ "unlock": ["B2:B9"]} # see SKILL.md Pitfalls
36
+ }
37
+ ]
38
+ }
39
+
40
+ Cell object keys (all optional except value/formula):
41
+ value scalar; JSON true/false -> bool, numbers stay numeric
42
+ type "date" or "datetime" -> value parsed from ISO string
43
+ formula e.g. "=SUM(A2:A9)" (leading '=' optional)
44
+ hyperlink URL; value becomes the display text
45
+ note cell note text (or {"text": ..., "author": ...})
46
+ format Excel number format, e.g. "$#,##0.00", "0.0%", "yyyy-mm-dd"
47
+ bold, italic booleans
48
+ font_size points
49
+ font_color hex RGB like "FF0000"
50
+ fill solid fill hex RGB like "DDEBF7"
51
+ border "thin" | "medium" | "thick" (all four sides)
52
+ align "left" | "center" | "right"
53
+ valign "top" | "center" | "bottom"
54
+ wrap boolean (wrap text)
55
+
56
+ Usage:
57
+ xlsx_create.py spec.json out.xlsx
58
+ xlsx_create.py - out.xlsx (spec on stdin)
59
+
60
+ Prints a JSON summary to stdout; exits non-zero on failure.
61
+ """
62
+ from __future__ import annotations
63
+
64
+ import argparse
65
+ import json
66
+ import sys
67
+ from datetime import date, datetime
68
+
69
+ from openpyxl import Workbook
70
+ from openpyxl.chart import BarChart, LineChart, PieChart, Reference
71
+ from openpyxl.comments import Comment
72
+ from openpyxl.formatting.rule import CellIsRule, ColorScaleRule
73
+ from openpyxl.styles import (Alignment, Border, Font, PatternFill,
74
+ Protection, Side)
75
+ from openpyxl.utils import column_index_from_string, range_boundaries
76
+ from openpyxl.workbook.defined_name import DefinedName
77
+ from openpyxl.worksheet.datavalidation import DataValidation
78
+ from openpyxl.worksheet.table import Table, TableStyleInfo
79
+
80
+
81
+ def parse_typed(value, type_hint=None):
82
+ if type_hint == "date" and isinstance(value, str):
83
+ return date.fromisoformat(value)
84
+ if type_hint == "datetime" and isinstance(value, str):
85
+ return datetime.fromisoformat(value)
86
+ return value
87
+
88
+
89
+ def apply_cell(ws, coord, spec):
90
+ cell = ws[coord]
91
+ if isinstance(spec, dict):
92
+ if "formula" in spec:
93
+ f = spec["formula"]
94
+ cell.value = f if f.startswith("=") else "=" + f
95
+ elif "value" in spec:
96
+ cell.value = parse_typed(spec["value"], spec.get("type"))
97
+ if "hyperlink" in spec:
98
+ cell.hyperlink = spec["hyperlink"]
99
+ if cell.value is None:
100
+ cell.value = spec["hyperlink"]
101
+ cell.style = "Hyperlink"
102
+ if "note" in spec:
103
+ note = spec["note"]
104
+ if isinstance(note, dict):
105
+ cell.comment = Comment(note.get("text", ""),
106
+ note.get("author", "xlsx-skill"))
107
+ else:
108
+ cell.comment = Comment(str(note), "xlsx-skill")
109
+ if "format" in spec:
110
+ cell.number_format = spec["format"]
111
+ font_kw = {}
112
+ if spec.get("bold"):
113
+ font_kw["bold"] = True
114
+ if spec.get("italic"):
115
+ font_kw["italic"] = True
116
+ if "font_size" in spec:
117
+ font_kw["size"] = spec["font_size"]
118
+ if "font_color" in spec:
119
+ font_kw["color"] = spec["font_color"]
120
+ if font_kw:
121
+ cell.font = Font(**font_kw)
122
+ if "fill" in spec:
123
+ cell.fill = PatternFill("solid", fgColor=spec["fill"])
124
+ if "border" in spec:
125
+ side = Side(style=spec["border"])
126
+ cell.border = Border(left=side, right=side, top=side, bottom=side)
127
+ align_kw = {}
128
+ if "align" in spec:
129
+ align_kw["horizontal"] = spec["align"]
130
+ if "valign" in spec:
131
+ align_kw["vertical"] = spec["valign"]
132
+ if spec.get("wrap"):
133
+ align_kw["wrap_text"] = True
134
+ if align_kw:
135
+ cell.alignment = Alignment(**align_kw)
136
+ else:
137
+ cell.value = spec
138
+
139
+
140
+ def ref_from_range(ws, rng):
141
+ min_col, min_row, max_col, max_row = range_boundaries(rng)
142
+ return Reference(ws, min_col=min_col, min_row=min_row,
143
+ max_col=max_col, max_row=max_row)
144
+
145
+
146
+ def add_chart(ws, spec):
147
+ kind = spec.get("type", "bar")
148
+ chart = {"bar": BarChart, "line": LineChart, "pie": PieChart}[kind]()
149
+ if "title" in spec:
150
+ chart.title = spec["title"]
151
+ data = ref_from_range(ws, spec["data"])
152
+ chart.add_data(data, titles_from_data=spec.get("titles_from_data", True))
153
+ if "categories" in spec:
154
+ chart.set_categories(ref_from_range(ws, spec["categories"]))
155
+ ws.add_chart(chart, spec.get("anchor", "H2"))
156
+
157
+
158
+ def add_conditional(ws, spec):
159
+ rng = spec["range"]
160
+ kind = spec.get("type", "cell_is")
161
+ if kind == "color_scale":
162
+ rule = ColorScaleRule(
163
+ start_type="min", start_color=spec.get("start_color", "FFF8696B"),
164
+ end_type="max", end_color=spec.get("end_color", "FF63BE7B"))
165
+ else:
166
+ fill = PatternFill("solid", fgColor=spec.get("fill", "FFC7CE"))
167
+ rule = CellIsRule(operator=spec.get("operator", "greaterThan"),
168
+ formula=spec.get("formula", ["0"]), fill=fill)
169
+ ws.conditional_formatting.add(rng, rule)
170
+
171
+
172
+ def build_sheet(ws, spec):
173
+ for row in spec.get("rows", []):
174
+ values, styled = [], []
175
+ for item in row:
176
+ if isinstance(item, dict):
177
+ values.append(None)
178
+ styled.append(item)
179
+ else:
180
+ values.append(item)
181
+ styled.append(None)
182
+ ws.append(values)
183
+ r = ws.max_row
184
+ for idx, item in enumerate(styled, start=1):
185
+ if item is not None:
186
+ apply_cell(ws, ws.cell(row=r, column=idx).coordinate, item)
187
+ for coord, cell_spec in spec.get("cells", {}).items():
188
+ apply_cell(ws, coord, cell_spec)
189
+ for col, width in spec.get("column_widths", {}).items():
190
+ ws.column_dimensions[col].width = width
191
+ for row, height in spec.get("row_heights", {}).items():
192
+ ws.row_dimensions[int(row)].height = height
193
+ for rng in spec.get("merges", []):
194
+ ws.merge_cells(rng)
195
+ if spec.get("freeze_panes"):
196
+ ws.freeze_panes = spec["freeze_panes"]
197
+ if spec.get("autofilter"):
198
+ ws.auto_filter.ref = spec["autofilter"]
199
+ for cf in spec.get("conditional_formats", []):
200
+ add_conditional(ws, cf)
201
+ for ch in spec.get("charts", []):
202
+ add_chart(ws, ch)
203
+ for dv_spec in spec.get("validations", []):
204
+ dv = DataValidation(type=dv_spec.get("type", "list"),
205
+ formula1=dv_spec["formula1"],
206
+ allow_blank=dv_spec.get("allow_blank", True))
207
+ dv.add(dv_spec["range"])
208
+ ws.add_data_validation(dv)
209
+ for t_spec in spec.get("tables", []):
210
+ table = Table(displayName=t_spec["name"], ref=t_spec["range"])
211
+ table.tableStyleInfo = TableStyleInfo(
212
+ name=t_spec.get("style", "TableStyleMedium9"),
213
+ showRowStripes=t_spec.get("row_stripes", True),
214
+ showColumnStripes=t_spec.get("column_stripes", False))
215
+ ws.add_table(table)
216
+ prot = spec.get("protection")
217
+ if prot:
218
+ for rng in prot.get("unlock", []):
219
+ for row in ws[rng]:
220
+ for cell in row:
221
+ cell.protection = Protection(locked=False)
222
+ if prot.get("password"):
223
+ ws.protection.password = prot["password"]
224
+ ws.protection.sheet = True
225
+
226
+
227
+ def main(argv=None):
228
+ ap = argparse.ArgumentParser(description="Create .xlsx from a JSON spec.")
229
+ ap.add_argument("spec", help="path to JSON spec, or '-' for stdin")
230
+ ap.add_argument("output", help="output .xlsx path")
231
+ args = ap.parse_args(argv)
232
+
233
+ if args.spec == "-":
234
+ spec = json.load(sys.stdin)
235
+ else:
236
+ with open(args.spec, encoding="utf-8") as fh:
237
+ spec = json.load(fh)
238
+
239
+ wb = Workbook()
240
+ wb.remove(wb.active)
241
+ for sheet_spec in spec.get("sheets", []):
242
+ ws = wb.create_sheet(sheet_spec.get("name", "Sheet1"))
243
+ build_sheet(ws, sheet_spec)
244
+ for name, ref in spec.get("defined_names", {}).items():
245
+ wb.defined_names[name] = DefinedName(name, attr_text=ref)
246
+ if spec.get("full_calc_on_load"):
247
+ wb.calculation.fullCalcOnLoad = True
248
+ wb.save(args.output)
249
+ print(json.dumps({"ok": True, "output": args.output,
250
+ "sheets": wb.sheetnames}, ensure_ascii=False))
251
+ return 0
252
+
253
+
254
+ if __name__ == "__main__":
255
+ try:
256
+ sys.exit(main())
257
+ except Exception as exc: # noqa: BLE001
258
+ print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
259
+ sys.exit(1)