dsh-office-kit 0.3.0 → 0.3.2

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 (73) hide show
  1. package/assets/skills/office-documents/render_docx.py +7 -0
  2. package/assets/skills/office-documents/scripts/a11y_audit.py +7 -0
  3. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +7 -0
  4. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +7 -0
  5. package/assets/skills/office-documents/scripts/append_blocks.py +7 -0
  6. package/assets/skills/office-documents/scripts/apply_template_styles.py +7 -0
  7. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +7 -0
  8. package/assets/skills/office-documents/scripts/comments_add.py +7 -0
  9. package/assets/skills/office-documents/scripts/comments_apply_patch.py +7 -0
  10. package/assets/skills/office-documents/scripts/comments_extract.py +7 -0
  11. package/assets/skills/office-documents/scripts/comments_strip.py +7 -0
  12. package/assets/skills/office-documents/scripts/content_controls.py +7 -0
  13. package/assets/skills/office-documents/scripts/core_properties.py +7 -0
  14. package/assets/skills/office-documents/scripts/docx_kit.py +7 -0
  15. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +7 -0
  16. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +7 -0
  17. package/assets/skills/office-documents/scripts/fields_materialize.py +7 -0
  18. package/assets/skills/office-documents/scripts/fields_report.py +7 -0
  19. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +7 -0
  20. package/assets/skills/office-documents/scripts/footnotes_report.py +7 -0
  21. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +7 -0
  22. package/assets/skills/office-documents/scripts/heading_audit.py +7 -0
  23. package/assets/skills/office-documents/scripts/images_audit.py +7 -0
  24. package/assets/skills/office-documents/scripts/insert_note.py +7 -0
  25. package/assets/skills/office-documents/scripts/insert_ref_fields.py +7 -0
  26. package/assets/skills/office-documents/scripts/insert_toc.py +7 -0
  27. package/assets/skills/office-documents/scripts/internal_nav.py +7 -0
  28. package/assets/skills/office-documents/scripts/make_fixtures.py +7 -0
  29. package/assets/skills/office-documents/scripts/merge_docx_append.py +7 -0
  30. package/assets/skills/office-documents/scripts/privacy_scrub.py +7 -0
  31. package/assets/skills/office-documents/scripts/redact_docx.py +7 -0
  32. package/assets/skills/office-documents/scripts/render_and_diff.py +7 -0
  33. package/assets/skills/office-documents/scripts/replace_text.py +7 -0
  34. package/assets/skills/office-documents/scripts/section_audit.py +7 -0
  35. package/assets/skills/office-documents/scripts/set_protection.py +7 -0
  36. package/assets/skills/office-documents/scripts/style_lint.py +7 -0
  37. package/assets/skills/office-documents/scripts/style_normalize.py +7 -0
  38. package/assets/skills/office-documents/scripts/table_geometry.py +7 -0
  39. package/assets/skills/office-documents/scripts/watermark_add.py +7 -0
  40. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +7 -0
  41. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +7 -0
  42. package/assets/skills/office-presentations/container_tools/add_slides.py +7 -0
  43. package/assets/skills/office-presentations/container_tools/core_properties.py +7 -0
  44. package/assets/skills/office-presentations/container_tools/create_montage.py +7 -0
  45. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +7 -0
  46. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +7 -0
  47. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +7 -0
  48. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +7 -0
  49. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +7 -0
  50. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +7 -0
  51. package/assets/skills/office-presentations/container_tools/preview_render.py +7 -0
  52. package/assets/skills/office-presentations/container_tools/render_slides.py +7 -0
  53. package/assets/skills/office-presentations/container_tools/replace_text.py +7 -0
  54. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +7 -0
  55. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +7 -0
  56. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +7 -0
  57. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +7 -0
  58. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +7 -0
  59. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +7 -0
  60. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +7 -0
  61. package/assets/skills/office-spreadsheets/container_tools/add_image.py +7 -0
  62. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +7 -0
  63. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +7 -0
  64. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +7 -0
  65. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +19 -7
  66. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +7 -0
  67. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +7 -0
  68. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +7 -0
  69. package/assets/skills/shared/check_runtime.py +7 -0
  70. package/dist/index.js +31 -18
  71. package/package.json +1 -1
  72. package/postinstall.mjs +41 -18
  73. package/python/office_cli.py +52 -9
@@ -13,6 +13,13 @@ import json
13
13
  import sys
14
14
  from pathlib import Path
15
15
 
16
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
17
+ for _stream in (sys.stdout, sys.stderr):
18
+ try:
19
+ _stream.reconfigure(encoding="utf-8", errors="replace")
20
+ except (AttributeError, ValueError):
21
+ pass
22
+
16
23
  try:
17
24
  from PIL import Image, ImageDraw, ImageFont
18
25
  except ImportError: # pragma: no cover - whitelist dependency, but fail clearly
@@ -42,6 +42,13 @@ from ooxml_helpers import ( # noqa: E402
42
42
  text_of,
43
43
  )
44
44
 
45
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
46
+ for _stream in (sys.stdout, sys.stderr):
47
+ try:
48
+ _stream.reconfigure(encoding="utf-8", errors="replace")
49
+ except (AttributeError, ValueError):
50
+ pass
51
+
45
52
  TITLE_OUTSIDE_MARGIN_IN = 0.0
46
53
  DEFAULT_TITLE_FLOOR_PT = 28.0
47
54
  DEFAULT_BODY_FLOOR_PT = 14.0
@@ -16,6 +16,13 @@ import zipfile
16
16
  from pathlib import Path
17
17
  from xml.etree import ElementTree
18
18
 
19
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
20
+ for _stream in (sys.stdout, sys.stderr):
21
+ try:
22
+ _stream.reconfigure(encoding="utf-8", errors="replace")
23
+ except (AttributeError, ValueError):
24
+ pass
25
+
19
26
  REL_NS = "{http://schemas.openxmlformats.org/package/2006/relationships}"
20
27
  CT_NS = "{http://schemas.openxmlformats.org/package/2006/content-types}"
21
28
  P_NS = "{http://schemas.openxmlformats.org/presentationml/2006/main}"
@@ -20,6 +20,13 @@ import zipfile
20
20
  from pathlib import Path
21
21
  from xml.etree import ElementTree
22
22
 
23
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
24
+ for _stream in (sys.stdout, sys.stderr):
25
+ try:
26
+ _stream.reconfigure(encoding="utf-8", errors="replace")
27
+ except (AttributeError, ValueError):
28
+ pass
29
+
23
30
  sys.path.insert(0, str(Path(__file__).resolve().parent))
24
31
 
25
32
  C_NS = "{http://schemas.openxmlformats.org/drawingml/2006/chart}"
@@ -22,6 +22,13 @@ from decimal import Decimal, InvalidOperation
22
22
  from pathlib import Path
23
23
  from xml.etree import ElementTree
24
24
 
25
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
26
+ for _stream in (sys.stdout, sys.stderr):
27
+ try:
28
+ _stream.reconfigure(encoding="utf-8", errors="replace")
29
+ except (AttributeError, ValueError):
30
+ pass
31
+
25
32
  sys.path.insert(0, str(Path(__file__).resolve().parent))
26
33
 
27
34
  C_NS = "{http://schemas.openxmlformats.org/drawingml/2006/chart}"
@@ -24,6 +24,13 @@ import sys
24
24
  from decimal import Decimal, InvalidOperation
25
25
  from pathlib import Path
26
26
 
27
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
28
+ for _stream in (sys.stdout, sys.stderr):
29
+ try:
30
+ _stream.reconfigure(encoding="utf-8", errors="replace")
31
+ except (AttributeError, ValueError):
32
+ pass
33
+
27
34
  sys.path.insert(0, str(Path(__file__).resolve().parent))
28
35
 
29
36
  TOTAL_PATTERN = re.compile(r"\btotal\b", re.IGNORECASE)
@@ -23,6 +23,13 @@ import sys
23
23
  from pathlib import Path
24
24
  from typing import Any, Iterable, Iterator
25
25
 
26
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
27
+ for _stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ _stream.reconfigure(encoding="utf-8", errors="replace")
30
+ except (AttributeError, ValueError):
31
+ pass
32
+
26
33
  EMU_PER_INCH = 914400
27
34
 
28
35
  # Anything that renders about a full em wide and wraps mid-token.
@@ -48,6 +48,13 @@ from ooxml_helpers import ( # noqa: E402
48
48
  WIDE_CHAR,
49
49
  )
50
50
 
51
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
52
+ for _stream in (sys.stdout, sys.stderr):
53
+ try:
54
+ _stream.reconfigure(encoding="utf-8", errors="replace")
55
+ except (AttributeError, ValueError):
56
+ pass
57
+
51
58
  INK = (0.13, 0.15, 0.18)
52
59
  MUTED = (0.42, 0.45, 0.50)
53
60
  RULE = (0.80, 0.82, 0.85)
@@ -23,6 +23,13 @@ import sys
23
23
  import tempfile
24
24
  from pathlib import Path
25
25
 
26
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
27
+ for _stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ _stream.reconfigure(encoding="utf-8", errors="replace")
30
+ except (AttributeError, ValueError):
31
+ pass
32
+
26
33
  sys.path.insert(0, str(Path(__file__).resolve().parent))
27
34
 
28
35
 
@@ -26,6 +26,13 @@ import json
26
26
  import sys
27
27
  from pathlib import Path
28
28
 
29
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
30
+ for _stream in (sys.stdout, sys.stderr):
31
+ try:
32
+ _stream.reconfigure(encoding="utf-8", errors="replace")
33
+ except (AttributeError, ValueError):
34
+ pass
35
+
29
36
 
30
37
  def _emit(payload: dict, code: int = 0) -> int:
31
38
  print(json.dumps(payload, ensure_ascii=False, indent=2))
@@ -30,6 +30,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
30
30
 
31
31
  from ooxml_helpers import box, is_picture, slide_size_in # noqa: E402
32
32
 
33
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
34
+ for _stream in (sys.stdout, sys.stderr):
35
+ try:
36
+ _stream.reconfigure(encoding="utf-8", errors="replace")
37
+ except (AttributeError, ValueError):
38
+ pass
39
+
33
40
  EMU_PER_INCH = 914400
34
41
  DEFAULT_TOLERANCE_EMU = 9144 # 0.01 in
35
42
 
@@ -20,6 +20,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools
20
20
 
21
21
  from ooxml_helpers import box, has_visible_text, is_picture, overlap_ratio, text_of # noqa: E402
22
22
 
23
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
24
+ for _stream in (sys.stdout, sys.stderr):
25
+ try:
26
+ _stream.reconfigure(encoding="utf-8", errors="replace")
27
+ except (AttributeError, ValueError):
28
+ pass
29
+
23
30
  PLACEHOLDER_PATTERNS = [
24
31
  re.compile(pattern, re.IGNORECASE)
25
32
  for pattern in (
@@ -23,6 +23,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools
23
23
 
24
24
  from ooxml_helpers import box, has_visible_text, is_chart, is_picture, is_table, placeholder_kind, slide_size_in, text_of # noqa: E402
25
25
 
26
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
27
+ for _stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ _stream.reconfigure(encoding="utf-8", errors="replace")
30
+ except (AttributeError, ValueError):
31
+ pass
32
+
26
33
 
27
34
  def _theme(presentation) -> dict:
28
35
  fonts: set[str] = set()
@@ -15,6 +15,13 @@ import math
15
15
  import sys
16
16
  from pathlib import Path
17
17
 
18
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
19
+ for _stream in (sys.stdout, sys.stderr):
20
+ try:
21
+ _stream.reconfigure(encoding="utf-8", errors="replace")
22
+ except (AttributeError, ValueError):
23
+ pass
24
+
18
25
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
19
26
 
20
27
 
@@ -23,6 +23,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools
23
23
 
24
24
  from ooxml_helpers import box, placeholder_kind, slide_size_in # noqa: E402
25
25
 
26
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
27
+ for _stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ _stream.reconfigure(encoding="utf-8", errors="replace")
30
+ except (AttributeError, ValueError):
31
+ pass
32
+
26
33
  PLACEHOLDER_ALIASES = {
27
34
  "title": ("TITLE", "CENTER_TITLE"),
28
35
  "subtitle": ("SUBTITLE",),
@@ -26,6 +26,13 @@ import json
26
26
  import sys
27
27
  from pathlib import Path
28
28
 
29
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
30
+ for _stream in (sys.stdout, sys.stderr):
31
+ try:
32
+ _stream.reconfigure(encoding="utf-8", errors="replace")
33
+ except (AttributeError, ValueError):
34
+ pass
35
+
29
36
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "container_tools"))
30
37
 
31
38
  ALLOWED_ACTIONS = {"rewrite", "rewrite-and-reposition", "replace", "delete", "keep", "fill-placeholder"}
@@ -35,6 +35,13 @@ import json
35
35
  import sys
36
36
  from pathlib import Path
37
37
 
38
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
39
+ for _stream in (sys.stdout, sys.stderr):
40
+ try:
41
+ _stream.reconfigure(encoding="utf-8", errors="replace")
42
+ except (AttributeError, ValueError):
43
+ pass
44
+
38
45
  if hasattr(sys.stdout, "reconfigure"):
39
46
  sys.stdout.reconfigure(errors="replace")
40
47
 
@@ -23,6 +23,13 @@ import json
23
23
  import sys
24
24
  from pathlib import Path
25
25
 
26
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
27
+ for _stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ _stream.reconfigure(encoding="utf-8", errors="replace")
30
+ except (AttributeError, ValueError):
31
+ pass
32
+
26
33
  if hasattr(sys.stdout, "reconfigure"):
27
34
  sys.stdout.reconfigure(errors="replace")
28
35
 
@@ -29,6 +29,13 @@ import json
29
29
  import sys
30
30
  from pathlib import Path
31
31
 
32
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
33
+ for _stream in (sys.stdout, sys.stderr):
34
+ try:
35
+ _stream.reconfigure(encoding="utf-8", errors="replace")
36
+ except (AttributeError, ValueError):
37
+ pass
38
+
32
39
  if hasattr(sys.stdout, "reconfigure"):
33
40
  sys.stdout.reconfigure(errors="replace")
34
41
 
@@ -21,6 +21,13 @@ import json
21
21
  import sys
22
22
  from pathlib import Path
23
23
 
24
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
25
+ for _stream in (sys.stdout, sys.stderr):
26
+ try:
27
+ _stream.reconfigure(encoding="utf-8", errors="replace")
28
+ except (AttributeError, ValueError):
29
+ pass
30
+
24
31
  if hasattr(sys.stdout, "reconfigure"):
25
32
  # Legacy console codepages (cp936, cp1252) must not abort a report.
26
33
  sys.stdout.reconfigure(errors="replace")
@@ -22,6 +22,13 @@ import sys
22
22
  import zipfile
23
23
  from pathlib import Path
24
24
 
25
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
26
+ for _stream in (sys.stdout, sys.stderr):
27
+ try:
28
+ _stream.reconfigure(encoding="utf-8", errors="replace")
29
+ except (AttributeError, ValueError):
30
+ pass
31
+
25
32
  if hasattr(sys.stdout, "reconfigure"):
26
33
  # Legacy console codepages (cp936, cp1252) must not abort a report.
27
34
  sys.stdout.reconfigure(errors="replace")
@@ -28,6 +28,13 @@ import subprocess
28
28
  import sys
29
29
  from pathlib import Path
30
30
 
31
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
32
+ for _stream in (sys.stdout, sys.stderr):
33
+ try:
34
+ _stream.reconfigure(encoding="utf-8", errors="replace")
35
+ except (AttributeError, ValueError):
36
+ pass
37
+
31
38
  if hasattr(sys.stdout, "reconfigure"):
32
39
  # Legacy console codepages (cp936, cp1252) must not abort a report.
33
40
  sys.stdout.reconfigure(errors="replace")
@@ -116,23 +123,28 @@ def _referenced_sheets(formula: str) -> list[str]:
116
123
  return found
117
124
 
118
125
 
119
- def _recalculate(python: str, cli: Path, source: Path, out: Path) -> tuple[bool, str | None]:
126
+ def _recalculate(python: str, cli: Path, source: Path, out: Path) -> tuple[bool, str | None, str | None]:
120
127
  command = [python, str(cli), "xlsx", "recalc", "--input", str(source), "--out", str(out)]
121
128
  try:
122
129
  completed = subprocess.run(command, capture_output=True, text=True, timeout=300)
123
130
  except FileNotFoundError as error:
124
- return False, f"cannot start the helper CLI: {error}"
131
+ return False, f"cannot start the helper CLI: {error}", None
125
132
  except subprocess.TimeoutExpired:
126
- return False, "recalculation timed out after 300 seconds"
133
+ return False, "recalculation timed out after 300 seconds", None
127
134
  if completed.returncode == 0 and out.is_file():
128
- return True, None
135
+ renderer = None
136
+ try:
137
+ renderer = json.loads(completed.stdout).get("renderer")
138
+ except (json.JSONDecodeError, AttributeError):
139
+ renderer = None
140
+ return True, None, renderer
129
141
  message = (completed.stderr or completed.stdout or "").strip()
130
142
  try:
131
143
  parsed = json.loads(message)
132
144
  message = str(parsed.get("error", message))
133
145
  except json.JSONDecodeError:
134
146
  pass
135
- return False, message or f"helper CLI exited with code {completed.returncode}"
147
+ return False, message or f"helper CLI exited with code {completed.returncode}", None
136
148
 
137
149
 
138
150
  def _ignored(entries: str | None) -> dict[str, set[str]]:
@@ -176,10 +188,10 @@ def main() -> int:
176
188
  if not cli.is_file():
177
189
  recalc_error = f"helper CLI not found: {cli}; pass --cli or use --no-recalc"
178
190
  else:
179
- recalculated, recalc_error = _recalculate(args.python, cli, source, out)
191
+ recalculated, recalc_error, renderer = _recalculate(args.python, cli, source, out)
180
192
  if recalculated:
181
193
  checked = out
182
- engine = "microsoft excel"
194
+ engine = renderer or "recalculation engine"
183
195
 
184
196
  try:
185
197
  cached = _cached_values(checked, cells) if cells else {}
@@ -26,6 +26,13 @@ import json
26
26
  import sys
27
27
  from pathlib import Path
28
28
 
29
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
30
+ for _stream in (sys.stdout, sys.stderr):
31
+ try:
32
+ _stream.reconfigure(encoding="utf-8", errors="replace")
33
+ except (AttributeError, ValueError):
34
+ pass
35
+
29
36
  if hasattr(sys.stdout, "reconfigure"):
30
37
  sys.stdout.reconfigure(errors="replace")
31
38
 
@@ -24,6 +24,13 @@ import json
24
24
  import sys
25
25
  from pathlib import Path
26
26
 
27
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
28
+ for _stream in (sys.stdout, sys.stderr):
29
+ try:
30
+ _stream.reconfigure(encoding="utf-8", errors="replace")
31
+ except (AttributeError, ValueError):
32
+ pass
33
+
27
34
  if hasattr(sys.stdout, "reconfigure"):
28
35
  sys.stdout.reconfigure(errors="replace")
29
36
 
@@ -30,6 +30,13 @@ import json
30
30
  import sys
31
31
  from pathlib import Path
32
32
 
33
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
34
+ for _stream in (sys.stdout, sys.stderr):
35
+ try:
36
+ _stream.reconfigure(encoding="utf-8", errors="replace")
37
+ except (AttributeError, ValueError):
38
+ pass
39
+
33
40
  if hasattr(sys.stdout, "reconfigure"):
34
41
  sys.stdout.reconfigure(errors="replace")
35
42
 
@@ -24,6 +24,13 @@ import subprocess
24
24
  import sys
25
25
  from pathlib import Path
26
26
 
27
+ # Windows consoles default to a legacy code page; every payload is UTF-8 JSON.
28
+ for _stream in (sys.stdout, sys.stderr):
29
+ try:
30
+ _stream.reconfigure(encoding="utf-8", errors="replace")
31
+ except (AttributeError, ValueError):
32
+ pass
33
+
27
34
  DEFAULT_ROOT = Path(__file__).resolve().parents[3]
28
35
  RENDERER_HINT = (
29
36
  "docx/pptx/xlsx rendering needs LibreOffice, Microsoft Office, or WPS Office; "
package/dist/index.js CHANGED
@@ -280,18 +280,13 @@ function statusOf(python) {
280
280
  remedy: runtimeRemedy()
281
281
  };
282
282
  }
283
- /** Inspect every candidate; return the first interpreter that proves it has all dependencies. */
284
- async function inspectRuntime(configFile = officePaths().configFile) {
285
- let compatible;
286
- let last;
287
- for (const candidate of [...candidates(configFile), ...await launcherInterpreters()]) for (const expanded of await expandLauncher(candidate)) {
288
- if (expanded !== candidate && !isFile(expanded)) continue;
289
- const inspection = await inspectPython(expanded);
290
- if (inspection.compatible && inspection.missing.length === 0) return statusOf(inspection);
291
- if (inspection.compatible && compatible == null) compatible = inspection;
292
- last = inspection;
293
- }
283
+ /** Pick the best interpreter: all dependencies first, then compatible-only, else none. */
284
+ function selectRuntime(inspections) {
285
+ const ready = inspections.find((inspection) => inspection.compatible && inspection.missing.length === 0);
286
+ if (ready != null) return statusOf(ready);
287
+ const compatible = inspections.find((inspection) => inspection.compatible);
294
288
  if (compatible != null) return statusOf(compatible);
289
+ const last = inspections[inspections.length - 1];
295
290
  return {
296
291
  state: "no-interpreter",
297
292
  packages: [],
@@ -300,6 +295,17 @@ async function inspectRuntime(configFile = officePaths().configFile) {
300
295
  ...last?.error == null ? {} : { error: last.error }
301
296
  };
302
297
  }
298
+ /** Inspect candidates in priority order; the first fully-provisioned interpreter wins. */
299
+ async function inspectRuntime(configFile = officePaths().configFile) {
300
+ const seen = [];
301
+ for (const candidate of [...candidates(configFile), ...await launcherInterpreters()]) for (const expanded of await expandLauncher(candidate)) {
302
+ if (expanded !== candidate && !isFile(expanded)) continue;
303
+ const inspection = await inspectPython(expanded);
304
+ seen.push(inspection);
305
+ if (inspection.compatible && inspection.missing.length === 0) return selectRuntime(seen);
306
+ }
307
+ return selectRuntime(seen);
308
+ }
303
309
  /** Run postinstall.mjs as a child process; resolve with an error summary or undefined. */
304
310
  function runPostinstall() {
305
311
  return new Promise((resolve) => {
@@ -324,20 +330,27 @@ function runPostinstall() {
324
330
  }
325
331
  let cache;
326
332
  let healing;
333
+ /** A cached `ok` is only worth reusing while its interpreter still exists on disk. */
334
+ function runtimeAlive(status) {
335
+ return status.state !== "ok" || status.python != null && isFile(status.python.path);
336
+ }
327
337
  /**
328
338
  * Resolve a runtime that is proven to work, self-healing via postinstall.mjs when it
329
339
  * is missing or incomplete. Never returns a bare interpreter without dependencies.
340
+ * Pass `force` for an explicit status check that must not trust the process cache.
330
341
  */
331
- function ensureRuntime(configFile = officePaths().configFile) {
332
- if (cache != null && (cache.status.state === "ok" || Date.now() - cache.at < FAILURE_RETRY_DELAY)) return Promise.resolve(cache.status);
342
+ function ensureRuntime(configFile = officePaths().configFile, force = false) {
343
+ if (!force && cache != null) {
344
+ if (cache.status.state === "ok" ? runtimeAlive(cache.status) : Date.now() - cache.at < FAILURE_RETRY_DELAY) return Promise.resolve(cache.status);
345
+ }
333
346
  healing ??= (async () => {
334
- const fresh = await inspectRuntime(configFile);
335
- if (fresh.state === "ok") {
347
+ const probed = await inspectRuntime(configFile);
348
+ if (probed.state === "ok") {
336
349
  cache = {
337
- status: fresh,
350
+ status: probed,
338
351
  at: Date.now()
339
352
  };
340
- return fresh;
353
+ return probed;
341
354
  }
342
355
  const healError = await runPostinstall();
343
356
  const status = {
@@ -519,7 +532,7 @@ function apply(ctx) {
519
532
  },
520
533
  timeoutMs: 6e5,
521
534
  async execute() {
522
- const runtime = await ensureRuntime(paths.configFile);
535
+ const runtime = await ensureRuntime(paths.configFile, true);
523
536
  const report = {
524
537
  plugin: {
525
538
  name,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-office-kit",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "description": "Office documents, PDF, presentations, and spreadsheets for DeepSeek Harness",
6
6
  "author": "Hairy",
7
7
  "license": "MIT",
package/postinstall.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { spawnSync } from 'node:child_process'
19
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
19
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
20
20
  import { homedir, tmpdir } from 'node:os'
21
21
  import { dirname, join } from 'node:path'
22
22
  import process from 'node:process'
@@ -25,15 +25,15 @@ import { fileURLToPath } from 'node:url'
25
25
  const PACKAGE_ROOT = dirname(fileURLToPath(import.meta.url))
26
26
  const IS_WINDOWS = process.platform === 'win32'
27
27
  const PYTHON_VERSION = '3.12.10'
28
- const RUNTIME_DIR = join(homedir(), '.dsh', 'office', 'runtime')
29
- const VENV_DIR = join(RUNTIME_DIR, 'venv')
28
+ export const RUNTIME_DIR = join(homedir(), '.dsh', 'office', 'runtime')
29
+ export const VENV_DIR = join(RUNTIME_DIR, 'venv')
30
30
  const VENV_PYTHON = join(VENV_DIR, IS_WINDOWS ? 'Scripts/python.exe' : 'bin/python')
31
31
  const LEGACY_DATA_DIR = IS_WINDOWS
32
32
  ? join(process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'), 'dsh-office-kit')
33
33
  : process.platform === 'darwin'
34
34
  ? join(homedir(), 'Library', 'Application Support', 'dsh-office-kit')
35
35
  : join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'dsh-office-kit')
36
- const LEGACY_VENV_DIR = join(LEGACY_DATA_DIR, 'venv')
36
+ export const LEGACY_VENV_DIR = join(LEGACY_DATA_DIR, 'venv')
37
37
  const LEGACY_VENV_PYTHON = join(LEGACY_VENV_DIR, IS_WINDOWS ? 'Scripts/python.exe' : 'bin/python')
38
38
  const CONFIG_DIR = join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'dsh-office-kit')
39
39
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
@@ -181,6 +181,24 @@ function verifySignature(file) {
181
181
  }
182
182
  }
183
183
 
184
+ /**
185
+ * Installer switches. PATH is appended by the installer itself (`AppendPath=1`),
186
+ * the install location stays the per-user default (no `TargetDir`), and the
187
+ * `py` launcher is kept so `py -3.12` works.
188
+ */
189
+ export function installerArguments() {
190
+ return [
191
+ '/quiet',
192
+ 'InstallAllUsers=0',
193
+ 'AppendPath=1',
194
+ 'CompileAll=0',
195
+ 'Include_test=0',
196
+ 'Include_doc=0',
197
+ 'Include_pip=1',
198
+ 'SimpleInstall=1',
199
+ ]
200
+ }
201
+
184
202
  function installPython() {
185
203
  if (!IS_WINDOWS) {
186
204
  return false
@@ -191,17 +209,7 @@ function installPython() {
191
209
  download(url, installer)
192
210
  verifySignature(installer)
193
211
  log(`installing Python ${PYTHON_VERSION} into the installer default per-user location`)
194
- const args = [
195
- '/quiet',
196
- 'InstallAllUsers=0',
197
- 'AppendPath=1',
198
- 'CompileAll=0',
199
- 'Include_test=0',
200
- 'Include_doc=0',
201
- 'Include_pip=1',
202
- 'SimpleInstall=1',
203
- ]
204
- const result = spawnSync(installer, args, { stdio: 'inherit', timeout: 15 * 60 * 1000 })
212
+ const result = spawnSync(installer, installerArguments(), { stdio: 'inherit', timeout: 15 * 60 * 1000 })
205
213
  rmSync(installer, { force: true })
206
214
  if (result.status !== 0) {
207
215
  throw new Error(`Python installer exited with status ${result.status}`)
@@ -368,6 +376,21 @@ function readPackageVersion() {
368
376
  }
369
377
  }
370
378
 
371
- const started = Date.now()
372
- main()
373
- log(`finished in ${((Date.now() - started) / 1000).toFixed(1)}s`)
379
+ function startedFromCli() {
380
+ const entry = process.argv[1]
381
+ if (entry == null) {
382
+ return false
383
+ }
384
+ try {
385
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url))
386
+ }
387
+ catch {
388
+ return false
389
+ }
390
+ }
391
+
392
+ if (startedFromCli()) {
393
+ const started = Date.now()
394
+ main()
395
+ log(`finished in ${((Date.now() - started) / 1000).toFixed(1)}s`)
396
+ }