markdown-docx 0.1.0__tar.gz → 0.2.0__tar.gz

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 (61) hide show
  1. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/.github/workflows/ci.yml +4 -0
  2. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/.github/workflows/publish-pypi.yml +4 -0
  3. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/CHANGELOG.md +7 -0
  4. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/PKG-INFO +37 -3
  5. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/README.md +35 -2
  6. markdown_docx-0.2.0/docs/skill-management.md +69 -0
  7. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/pyproject.toml +2 -1
  8. markdown_docx-0.2.0/scripts/smoke_skill.py +77 -0
  9. markdown_docx-0.2.0/src/markdown_docx/__init__.py +1 -0
  10. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/assets/syntax.json +2 -2
  11. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/cli.py +45 -13
  12. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/markdown_body.py +1 -1
  13. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/parser.py +2 -2
  14. markdown_docx-0.2.0/src/markdown_docx/skill.py +421 -0
  15. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/conftest.py +9 -0
  16. markdown_docx-0.2.0/tests/test_cli.py +298 -0
  17. markdown_docx-0.2.0/tests/test_skill.py +487 -0
  18. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/uv.lock +3 -1
  19. markdown_docx-0.1.0/src/markdown_docx/__init__.py +0 -1
  20. markdown_docx-0.1.0/src/markdown_docx/skill.py +0 -123
  21. markdown_docx-0.1.0/tests/test_cli.py +0 -163
  22. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/.gitignore +0 -0
  23. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/AGENTS.md +0 -0
  24. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/LICENSE +0 -0
  25. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/PLAN.md +0 -0
  26. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/docs/public-api-capabilities.md +0 -0
  27. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-lunch-chase.png +0 -0
  28. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-run-finish.png +0 -0
  29. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-run-start.png +0 -0
  30. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-trio-cameo.png +0 -0
  31. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-trio-inline.png +0 -0
  32. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/dog-wagon-rescue.png +0 -0
  33. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/illustration-prompts.md +0 -0
  34. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/word-icon.png +0 -0
  35. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/assets/word-workflow.png +0 -0
  36. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/showcase.docx +0 -0
  37. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/sample/showcase.md +0 -0
  38. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/Export-DocxPdf.ps1 +0 -0
  39. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/build_default_template.py +0 -0
  40. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/build_qa_templates.py +0 -0
  41. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/scripts/build_showcase_assets.py +0 -0
  42. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/assets/default.docx +0 -0
  43. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/assets.py +0 -0
  44. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/errors.py +0 -0
  45. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/images.py +0 -0
  46. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/metadata.py +0 -0
  47. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/models.py +0 -0
  48. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/renderer.py +0 -0
  49. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/styles.py +0 -0
  50. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/src/markdown_docx/template.py +0 -0
  51. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_metadata.py +0 -0
  52. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_parser.py +0 -0
  53. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_public_api_boundary.py +0 -0
  54. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_public_api_capabilities.py +0 -0
  55. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_images.py +0 -0
  56. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_lists.py +0 -0
  57. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_sections.py +0 -0
  58. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_tables.py +0 -0
  59. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_renderer_text.py +0 -0
  60. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_showcase.py +0 -0
  61. {markdown_docx-0.1.0 → markdown_docx-0.2.0}/tests/test_template.py +0 -0
@@ -60,6 +60,9 @@ jobs:
60
60
  - name: Run tests
61
61
  run: uv run pytest
62
62
 
63
+ - name: Smoke-test editable skill lifecycle
64
+ run: uv run python scripts/smoke_skill.py --expected-source local
65
+
63
66
  visual-smoke:
64
67
  runs-on: ubuntu-latest
65
68
  steps:
@@ -115,6 +118,7 @@ jobs:
115
118
  .wheel-smoke/bin/python -m pip install dist/*.whl
116
119
  .wheel-smoke/bin/markdown-docx --version
117
120
  .wheel-smoke/bin/markdown-docx --syntax --json
121
+ .wheel-smoke/bin/python scripts/smoke_skill.py
118
122
  echo '# Wheel smoke test' > smoke.md
119
123
  .wheel-smoke/bin/markdown-docx smoke.md smoke.docx
120
124
 
@@ -42,6 +42,9 @@ jobs:
42
42
  - name: Run tests
43
43
  run: uv run pytest
44
44
 
45
+ - name: Smoke-test editable skill lifecycle
46
+ run: uv run python scripts/smoke_skill.py --expected-source local
47
+
45
48
  - name: Verify release tag matches package version
46
49
  if: github.event_name == 'release' && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
47
50
  shell: bash
@@ -83,6 +86,7 @@ jobs:
83
86
  .wheel-smoke/bin/python -m pip install dist/*.whl
84
87
  .wheel-smoke/bin/markdown-docx --version
85
88
  .wheel-smoke/bin/markdown-docx --inspect-template --json
89
+ .wheel-smoke/bin/python scripts/smoke_skill.py
86
90
 
87
91
  - name: Upload distributions
88
92
  uses: actions/upload-artifact@v7.0.1
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Synchronize existing pristine managed skills to the running CLI version during normal commands.
6
+ - Store managed ownership, version, and normalized content hashes in `SKILL.md` front matter. Migrate legacy managed skills and recover invalid version metadata.
7
+ - Add read-only skill status and force installation for managed edits. Preserve custom directory support, removal safety, and JSON output.
8
+ - Skip automatic synchronization for local source and editable builds. Add atomic replacement, concurrent-change checks, and installed-wheel lifecycle smoke tests.
9
+
3
10
  ## 0.1.0
4
11
 
5
12
  - Add strict Markdown parsing with invisible YAML directives and line-aware diagnostics.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: markdown-docx
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Convert constrained Markdown documents into editable Word files.
5
5
  Project-URL: Homepage, https://github.com/pseudosavant/markdown-docx
6
6
  Project-URL: Repository, https://github.com/pseudosavant/markdown-docx
@@ -24,6 +24,7 @@ Classifier: Topic :: Text Processing :: Markup
24
24
  Requires-Python: >=3.11
25
25
  Requires-Dist: httpx<1,>=0.27
26
26
  Requires-Dist: markdown-it-py<5,>=3.0
27
+ Requires-Dist: packaging>=24.0
27
28
  Requires-Dist: pillow<13,>=10.0
28
29
  Requires-Dist: python-docx==1.2.0
29
30
  Requires-Dist: pyyaml<7,>=6.0
@@ -53,6 +54,39 @@ Then ask an agent to create both the Markdown source and the editable Word outpu
53
54
 
54
55
  The skill teaches the agent how to inspect the format, inspect blank Word templates, render safely, and handle structured results.
55
56
 
57
+ ## Manage the agent skill
58
+
59
+ The standard location is `~/.agents/skills/markdown-docx/SKILL.md`. Normally installed CLI builds automatically synchronize an already-installed managed skill during ordinary commands, including help and version output. The running CLI version is the authority. A pristine older skill is replaced with the bundled skill. Equal and newer versions are left alone. Missing and unmanaged skills are never installed or overwritten automatically.
60
+
61
+ Synchronization is local. It does not query a package index, refresh uv's cache, or update the CLI. The skill continues to instruct agents to use `uvx markdown-docx`. Updates affect future skill loading. Instructions already loaded by a running agent may stay unchanged until the agent reloads them.
62
+
63
+ Inspect the skill without changing it:
64
+
65
+ ```powershell
66
+ uvx markdown-docx skill status
67
+ uvx markdown-docx skill status --json
68
+ ```
69
+
70
+ Status reports the selected path, ownership, CLI and skill versions, version ordering, integrity, and automatic synchronization eligibility. Modified or unverifiable skills with valid version metadata are preserved. To replace a managed skill intentionally:
71
+
72
+ ```powershell
73
+ uvx markdown-docx skill install --force
74
+ ```
75
+
76
+ Install `--force` still refuses unmanaged content and never downgrades a newer skill. A normal install creates a missing skill, updates a pristine older skill, or leaves a current skill alone. Legacy managed skills and managed skills with missing or invalid version metadata receive a fresh replacement. This recovery does not require a valid hash.
77
+
78
+ Custom directories require explicit updates. Automatic synchronization only checks the standard directory. Local source and editable development builds skip automatic synchronization, but explicit installation still works:
79
+
80
+ ```powershell
81
+ uvx markdown-docx skill install --skills-dir PATH
82
+ uvx markdown-docx skill status --skills-dir PATH
83
+ uvx --from . markdown-docx skill install
84
+ ```
85
+
86
+ Remove the skill with `uvx markdown-docx skill remove`. Removal refuses unmanaged content and extra files unless `--force` is supplied. All skill commands accept `--skills-dir` and `--json`. Skill commands never trigger automatic synchronization. Maintenance notices go to stderr and leave JSON stdout intact.
87
+
88
+ See [skill lifecycle metadata and decisions](docs/skill-management.md) for the hash format and recovery rules.
89
+
56
90
  ## Use the CLI directly
57
91
 
58
92
  ```powershell
@@ -74,7 +108,7 @@ uvx markdown-docx --syntax --json
74
108
 
75
109
  ## Supported Markdown
76
110
 
77
- Version 0.1.0 supports:
111
+ Version 0.2.0 supports:
78
112
 
79
113
  - ATX headings from `#` through `######`
80
114
  - Paragraphs and standard soft or hard line breaks
@@ -86,7 +120,7 @@ Version 0.1.0 supports:
86
120
  - Local and remote inline images
87
121
  - Standalone images with width and alignment metadata
88
122
 
89
- Links are rejected in 0.1.0. `python-docx` 1.2.0 can read hyperlinks but has no supported public API for creating them. The source alt text for images remains meaningful Markdown content, but the same library release has no public API for embedding it in a Word drawing. A rendered document containing images reports `image_alt_text_not_embedded` in its warning list.
123
+ Links are rejected in 0.2.0. `python-docx` 1.2.0 can read hyperlinks but has no supported public API for creating them. The source alt text for images remains meaningful Markdown content, but the same library release has no public API for embedding it in a Word drawing. A rendered document containing images reports `image_alt_text_not_embedded` in its warning list.
90
124
 
91
125
  The following syntax is intentionally unsupported:
92
126
 
@@ -22,6 +22,39 @@ Then ask an agent to create both the Markdown source and the editable Word outpu
22
22
 
23
23
  The skill teaches the agent how to inspect the format, inspect blank Word templates, render safely, and handle structured results.
24
24
 
25
+ ## Manage the agent skill
26
+
27
+ The standard location is `~/.agents/skills/markdown-docx/SKILL.md`. Normally installed CLI builds automatically synchronize an already-installed managed skill during ordinary commands, including help and version output. The running CLI version is the authority. A pristine older skill is replaced with the bundled skill. Equal and newer versions are left alone. Missing and unmanaged skills are never installed or overwritten automatically.
28
+
29
+ Synchronization is local. It does not query a package index, refresh uv's cache, or update the CLI. The skill continues to instruct agents to use `uvx markdown-docx`. Updates affect future skill loading. Instructions already loaded by a running agent may stay unchanged until the agent reloads them.
30
+
31
+ Inspect the skill without changing it:
32
+
33
+ ```powershell
34
+ uvx markdown-docx skill status
35
+ uvx markdown-docx skill status --json
36
+ ```
37
+
38
+ Status reports the selected path, ownership, CLI and skill versions, version ordering, integrity, and automatic synchronization eligibility. Modified or unverifiable skills with valid version metadata are preserved. To replace a managed skill intentionally:
39
+
40
+ ```powershell
41
+ uvx markdown-docx skill install --force
42
+ ```
43
+
44
+ Install `--force` still refuses unmanaged content and never downgrades a newer skill. A normal install creates a missing skill, updates a pristine older skill, or leaves a current skill alone. Legacy managed skills and managed skills with missing or invalid version metadata receive a fresh replacement. This recovery does not require a valid hash.
45
+
46
+ Custom directories require explicit updates. Automatic synchronization only checks the standard directory. Local source and editable development builds skip automatic synchronization, but explicit installation still works:
47
+
48
+ ```powershell
49
+ uvx markdown-docx skill install --skills-dir PATH
50
+ uvx markdown-docx skill status --skills-dir PATH
51
+ uvx --from . markdown-docx skill install
52
+ ```
53
+
54
+ Remove the skill with `uvx markdown-docx skill remove`. Removal refuses unmanaged content and extra files unless `--force` is supplied. All skill commands accept `--skills-dir` and `--json`. Skill commands never trigger automatic synchronization. Maintenance notices go to stderr and leave JSON stdout intact.
55
+
56
+ See [skill lifecycle metadata and decisions](docs/skill-management.md) for the hash format and recovery rules.
57
+
25
58
  ## Use the CLI directly
26
59
 
27
60
  ```powershell
@@ -43,7 +76,7 @@ uvx markdown-docx --syntax --json
43
76
 
44
77
  ## Supported Markdown
45
78
 
46
- Version 0.1.0 supports:
79
+ Version 0.2.0 supports:
47
80
 
48
81
  - ATX headings from `#` through `######`
49
82
  - Paragraphs and standard soft or hard line breaks
@@ -55,7 +88,7 @@ Version 0.1.0 supports:
55
88
  - Local and remote inline images
56
89
  - Standalone images with width and alignment metadata
57
90
 
58
- Links are rejected in 0.1.0. `python-docx` 1.2.0 can read hyperlinks but has no supported public API for creating them. The source alt text for images remains meaningful Markdown content, but the same library release has no public API for embedding it in a Word drawing. A rendered document containing images reports `image_alt_text_not_embedded` in its warning list.
91
+ Links are rejected in 0.2.0. `python-docx` 1.2.0 can read hyperlinks but has no supported public API for creating them. The source alt text for images remains meaningful Markdown content, but the same library release has no public API for embedding it in a Word drawing. A rendered document containing images reports `image_alt_text_not_embedded` in its warning list.
59
92
 
60
93
  The following syntax is intentionally unsupported:
61
94
 
@@ -0,0 +1,69 @@
1
+ # Managed agent skill
2
+
3
+ The distribution, CLI command, and skill are named `markdown-docx`. The Python import package is `markdown_docx`. `markdown_docx.__version__` supplies the version reported by the CLI and written by the canonical skill renderer in `src/markdown_docx/skill.py`.
4
+
5
+ ## Metadata
6
+
7
+ New installations write lifecycle metadata inside the existing YAML front matter:
8
+
9
+ ```yaml
10
+ ---
11
+ name: markdown-docx
12
+ description: Existing skill description
13
+ metadata:
14
+ managed-by: markdown-docx
15
+ managed-version: "0.2.0"
16
+ managed-content-sha256: "sha256:<64 lowercase hexadecimal characters>"
17
+ ---
18
+ ```
19
+
20
+ The description and all instructions come from one bundled template. The version is always a quoted string with the exact running CLI version. There is no top-level version field or sidecar file. New files use UTF-8 without a BOM, LF line endings, and a trailing newline.
21
+
22
+ To calculate the hash, render the complete file with `managed-content-sha256: ""`, normalize CRLF and CR to LF, then calculate SHA-256 over the UTF-8 bytes. Replace only the hash value with `sha256:` followed by the digest. Verification blanks that value using parsed YAML source positions. It preserves the rest of the original text rather than serializing YAML again. The hash covers the instructions, examples, front matter, and formatting. It detects edits and is not a signature or security boundary.
23
+
24
+ Front matter ownership is authoritative. The old `<!-- managed-by: markdown-docx -->` marker still identifies legacy managed files when no conflicting `managed-by` field is present. New files omit that marker. Unrelated metadata in the bundled template remains part of the generated skill and its hash.
25
+
26
+ ## Automatic decisions
27
+
28
+ All normal CLI invocations check `~/.agents/skills/markdown-docx/SKILL.md`, including rendering, inspection, help, version, about, and no-argument help. Skill commands do not run this check.
29
+
30
+ | Installed state | Automatic action |
31
+ | --- | --- |
32
+ | Missing directory or file | Leave absent |
33
+ | Unmanaged or owned by another tool | Leave untouched |
34
+ | Legacy managed file without a version | Treat as version 0 and replace |
35
+ | Managed file with missing or invalid version | Replace without hash verification |
36
+ | Equal or newer valid version | Leave untouched |
37
+ | Older version with matching stored hash | Replace with the running CLI's bundled skill |
38
+ | Older version with missing, malformed, or mismatched hash | Preserve and recommend force installation |
39
+
40
+ Versions use PEP 440 ordering through `packaging.version.Version`. An invalid running CLI version disables automatic synchronization. Verification compares the installed file against its own stored hash. It never compares an older file against the current bundled hash to decide whether the older file was edited.
41
+
42
+ Replacements use a flushed and closed temporary file in the skill directory followed by an atomic replacement. The installed file is read again immediately before replacement. Any observed concurrent change cancels the replacement. There is no lock or long retry loop. An external writer can still race the final filesystem operation.
43
+
44
+ Updates and preservation notices go to stderr. Failures are best effort and do not change the primary command's exit status or JSON stdout. No notice is emitted for absent, unmanaged, current, or newer skills, or skipped development builds.
45
+
46
+ ## Explicit commands
47
+
48
+ ```powershell
49
+ uvx markdown-docx skill install
50
+ uvx markdown-docx skill install --force
51
+ uvx markdown-docx skill status --json
52
+ uvx markdown-docx skill remove
53
+ ```
54
+
55
+ A normal install creates a missing skill and updates a pristine older skill. It refuses altered or unverifiable managed files with valid versions, including edits to the current version. Use `uvx markdown-docx skill install --force` to restore such a file. Force installation still refuses unmanaged content and never replaces a newer version. Missing or invalid managed versions use the recovery rule in the table.
56
+
57
+ Status is read-only. Plain and JSON results include the selected path, standard-location flag, installation and ownership state, exact CLI version, valid installed version, version relation, integrity state, runtime source, automatic synchronization eligibility and reason, and a force command when applicable. Missing or malformed managed versions are reported as null with a separate version state. Legacy status reports legacy integrity and compares its effective version 0 before migration.
58
+
59
+ Install retains the existing `installed`, `created`, `updated`, `skill`, and `path` JSON fields. Removal retains its existing result fields and force semantics. It refuses unmanaged skills or extra directory entries unless removal `--force` is supplied. Legacy managed skills remain removable. Installation and synchronization only write `SKILL.md`. They preserve unrelated files. Existing directories without `SKILL.md` are refused by explicit install and removal.
60
+
61
+ All three commands accept `--skills-dir PATH`. Custom directories participate only in explicit commands. Include the same directory when updating or forcing a custom installation.
62
+
63
+ ## Runtime source and loading
64
+
65
+ Automatic synchronization uses installed-distribution records and PEP 610 `direct_url.json`. Local directories, local source archives, and editable installations are excluded. Source code that does not match the installed distribution's module path is also excluded. Unidentified or malformed provenance is handled conservatively. An installed wheel, including one installed from a local wheel file, remains eligible. No launcher detection is used.
66
+
67
+ Explicit installation remains available from a checkout with `uvx --from . markdown-docx skill install`. Tests redirect the standard skills directory to temporary storage. The package smoke check verifies wheel provenance, runtime version, canonical content, and lifecycle behavior in an isolated temporary skill directory.
68
+
69
+ This feature only synchronizes local skill content to the CLI that is already running. It never queries PyPI, updates the CLI, or refreshes uv's cache. Agent instructions continue to invoke `uvx markdown-docx`. Updates apply to future skill loading and may not affect instructions already loaded in an active agent session.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "markdown-docx"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Convert constrained Markdown documents into editable Word files."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -28,6 +28,7 @@ classifiers = [
28
28
  dependencies = [
29
29
  "httpx>=0.27,<1",
30
30
  "markdown-it-py>=3.0,<5",
31
+ "packaging>=24.0",
31
32
  "Pillow>=10.0,<13",
32
33
  "PyYAML>=6.0,<7",
33
34
  "python-docx==1.2.0",
@@ -0,0 +1,77 @@
1
+ """Check packaged skill behavior without accessing the user's skills directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import io
7
+ import json
8
+ import tempfile
9
+ from importlib.metadata import version
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+ from markdown_docx import __version__, skill
14
+ from markdown_docx.cli import main
15
+
16
+
17
+ def invoke(args: list[str]) -> tuple[str, str]:
18
+ stdout, stderr = io.StringIO(), io.StringIO()
19
+ code = main(args, stdout=stdout, stderr=stderr)
20
+ assert code == 0, (code, stdout.getvalue(), stderr.getvalue())
21
+ return stdout.getvalue(), stderr.getvalue()
22
+
23
+
24
+ def check(expected_source: str) -> None:
25
+ assert skill.runtime_source() == expected_source
26
+ assert version("markdown-docx") == __version__
27
+ with tempfile.TemporaryDirectory(prefix="markdown-docx-skill-smoke-") as directory:
28
+ root = Path(directory) / "skills"
29
+ with patch.object(skill, "default_skills_dir", return_value=root):
30
+ stdout, stderr = invoke(["--version"])
31
+ assert stdout == f"markdown-docx {__version__}\n" and not stderr
32
+ assert not root.exists()
33
+ stdout, stderr = invoke(["skill", "install", "--json"])
34
+ installed = json.loads(stdout)
35
+ assert installed["created"] and not stderr
36
+ path = Path(installed["path"])
37
+ canonical = path.read_bytes()
38
+ assert canonical == skill.render_skill().encode("utf-8")
39
+ assert b"Always invoke the tool as `uvx markdown-docx ...`" in canonical
40
+ assert list(path.parent.iterdir()) == [path]
41
+ stdout, stderr = invoke(["skill", "status", "--json"])
42
+ status = json.loads(stdout)
43
+ assert status["managed_version"] == __version__
44
+ assert status["integrity"] == "valid" and not stderr
45
+ assert status["local_development"] == (expected_source == "local")
46
+ older = skill.render_skill("0.0.0").encode("utf-8")
47
+ path.write_bytes(older)
48
+ stdout, stderr = invoke(["--syntax", "--json"])
49
+ assert json.loads(stdout)["ok"]
50
+ if expected_source == "installed":
51
+ assert "Updated managed skill" in stderr
52
+ assert path.read_bytes() == canonical
53
+ else:
54
+ assert not stderr
55
+ assert path.read_bytes() == older
56
+ altered = older + b"Local edits\n"
57
+ path.write_bytes(altered)
58
+ stdout, stderr = invoke(["--syntax", "--json"])
59
+ assert json.loads(stdout)["ok"]
60
+ assert path.read_bytes() == altered
61
+ if expected_source == "installed":
62
+ assert skill.FORCE_INSTALL_COMMAND in stderr
63
+ else:
64
+ assert not stderr
65
+ stdout, stderr = invoke(["skill", "install", "--force", "--json"])
66
+ assert json.loads(stdout)["updated"] and not stderr
67
+ assert path.read_bytes() == canonical
68
+ stdout, stderr = invoke(["skill", "remove", "--json"])
69
+ assert json.loads(stdout)["removed"] and not stderr
70
+ assert not path.exists()
71
+ print(f"Managed skill smoke passed for {expected_source} runtime {__version__}")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ parser = argparse.ArgumentParser(description=__doc__)
76
+ parser.add_argument("--expected-source", choices=("installed", "local"), default="installed")
77
+ check(parser.parse_args().expected_source)
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "markdown-docx",
3
- "version": "0.1.0",
4
- "text": "markdown-docx 0.1.0 syntax\n\nSupported Markdown:\n ATX headings, paragraphs, emphasis, strong text, inline code, hard line breaks, fenced code blocks, blockquotes, ordered and unordered lists, pipe tables, and images.\n\nMetadata comments:\n document metadata must be first\n section metadata starts a next-page section\n <!-- markdown-docx: page-break --> inserts a page break\n table metadata must immediately precede a pipe table\n image metadata must immediately precede a standalone image\n\nLengths use in, cm, mm, or pt. Image widths may also use percentages.\n\nUnsupported in 0.1.0:\n links, raw HTML, task lists, footnotes, horizontal rules, indented code blocks, multi-paragraph list items, DOTX, floating images, headers and footers from Markdown, page-number fields, and direct OOXML features.",
3
+ "version": "0.2.0",
4
+ "text": "markdown-docx 0.2.0 syntax\n\nSupported Markdown:\n ATX headings, paragraphs, emphasis, strong text, inline code, hard line breaks, fenced code blocks, blockquotes, ordered and unordered lists, pipe tables, and images.\n\nMetadata comments:\n document metadata must be first\n section metadata starts a next-page section\n <!-- markdown-docx: page-break --> inserts a page break\n table metadata must immediately precede a pipe table\n image metadata must immediately precede a standalone image\n\nLengths use in, cm, mm, or pt. Image widths may also use percentages.\n\nUnsupported in 0.2.0:\n links, raw HTML, task lists, footnotes, horizontal rules, indented code blocks, multi-paragraph list items, DOTX, floating images, headers and footers from Markdown, page-number fields, and direct OOXML features.",
5
5
  "page_sizes": ["letter", "legal", "a4", "custom"],
6
6
  "orientations": ["portrait", "landscape"],
7
7
  "length_units": ["in", "cm", "mm", "pt"],
@@ -12,7 +12,7 @@ from markdown_docx.assets import load_syntax_payload
12
12
  from markdown_docx.errors import EXIT_INTERNAL, InputError, MarkdownDocxError, UsageError
13
13
  from markdown_docx.parser import parse_document
14
14
  from markdown_docx.renderer import render_docx
15
- from markdown_docx.skill import install_skill, remove_skill
15
+ from markdown_docx.skill import install_skill, remove_skill, skill_status, synchronize_skill
16
16
  from markdown_docx.template import inspect_template
17
17
 
18
18
  PROGRAM_NAME = "markdown-docx"
@@ -77,8 +77,10 @@ Inspection:
77
77
  {PROGRAM_NAME} --list-table-styles [--template formatting.docx] [--json]
78
78
 
79
79
  Agent skill:
80
- {PROGRAM_NAME} skill install [--skills-dir DIR] [--json]
80
+ {PROGRAM_NAME} skill install [--skills-dir DIR] [--force] [--json]
81
81
  {PROGRAM_NAME} skill remove [--skills-dir DIR] [--force] [--json]
82
+ {PROGRAM_NAME} skill status [--skills-dir DIR] [--json]
83
+ Installed managed skills sync locally during normal commands. See 'skill --help'.
82
84
 
83
85
  Common options:
84
86
  -h, --help Show this quick reference.
@@ -113,11 +115,17 @@ def main(
113
115
  args_list = list(sys.argv[1:] if argv is None else argv)
114
116
  json_mode = "--json" in args_list
115
117
  try:
118
+ if args_list and args_list[0] == "skill":
119
+ if len(args_list) == 1 or "-h" in args_list or "--help" in args_list:
120
+ stdout.write(build_skill_help())
121
+ return 0
122
+ return _run_skill_command(args_list[1:], stdout=stdout)
123
+ synchronize_skill(stderr=stderr, version=__version__)
116
124
  if not args_list:
117
125
  stdout.write(build_root_help())
118
126
  return 0
119
127
  if "-h" in args_list or "--help" in args_list:
120
- stdout.write(build_skill_help() if args_list[0] == "skill" else build_root_help())
128
+ stdout.write(build_root_help())
121
129
  return 0
122
130
  if "--version" in args_list:
123
131
  if args_list != ["--version"]:
@@ -131,8 +139,6 @@ def main(
131
139
  f"{PROGRAM_NAME} {__version__}\n{PROJECT_SUMMARY}\nProject: {PROJECT_URL}\nLicense: {PROJECT_LICENSE}\n"
132
140
  )
133
141
  return 0
134
- if args_list[0] == "skill":
135
- return _run_skill_command(args_list[1:], stdout=stdout)
136
142
  args = build_parser().parse_args(args_list)
137
143
  return _run(args, stdin=stdin, stdout=stdout)
138
144
  except MarkdownDocxError as exc:
@@ -287,30 +293,56 @@ def _write_error(exc: MarkdownDocxError, *, json_mode: bool, stdout: TextIO, std
287
293
 
288
294
  def build_skill_help() -> str:
289
295
  return f"""Usage:
290
- {PROGRAM_NAME} skill install [--skills-dir DIR] [--json]
296
+ {PROGRAM_NAME} skill install [--skills-dir DIR] [--force] [--json]
291
297
  {PROGRAM_NAME} skill remove [--skills-dir DIR] [--force] [--json]
292
-
293
- Install or remove the managed `{PROGRAM_NAME}` agent skill. The default root is ~/.agents/skills.
294
- Removal refuses unmanaged content unless --force is supplied.
298
+ {PROGRAM_NAME} skill status [--skills-dir DIR] [--json]
299
+
300
+ Install, inspect, or remove the managed `{PROGRAM_NAME}` agent skill.
301
+ The default root is ~/.agents/skills. Status is read-only.
302
+
303
+ Normal commands synchronize an already-installed, pristine older managed skill
304
+ to the running CLI version. They never install a missing skill or downgrade it.
305
+ Modified or unverifiable skills are preserved. To replace managed edits, use:
306
+ uvx {PROGRAM_NAME} skill install --force
307
+ Install --force still refuses unmanaged content. Removal --force may remove
308
+ unmanaged content and extra files in the skill directory.
309
+
310
+ Automatic synchronization uses only the standard directory. Custom directories
311
+ require explicit installs. Local source and editable builds do not synchronize
312
+ automatically. Skill commands never trigger automatic synchronization.
313
+ This does not query a package index, refresh uv, or update the CLI. Updates apply
314
+ to future agent skill loading. A running agent may retain its loaded instructions.
295
315
  """
296
316
 
297
317
 
298
318
  def _run_skill_command(args_list: list[str], *, stdout: TextIO) -> int:
299
319
  parser = CliArgumentParser(prog=f"{PROGRAM_NAME} skill", add_help=False)
300
- parser.add_argument("action", choices=("install", "remove"))
320
+ parser.add_argument("action", choices=("install", "remove", "status"))
301
321
  parser.add_argument("--skills-dir", type=Path)
302
322
  parser.add_argument("--force", action="store_true")
303
323
  parser.add_argument("--json", action="store_true")
304
324
  args = parser.parse_args(args_list)
305
- if args.action == "install" and args.force:
306
- raise UsageError("--force is valid only with 'skill remove'.")
325
+ if args.action == "status" and args.force:
326
+ raise UsageError("--force is valid only with 'skill install' or 'skill remove'.")
307
327
  root = args.skills_dir.resolve() if args.skills_dir else None
308
- result = install_skill(root) if args.action == "install" else remove_skill(root, force=args.force)
328
+ if args.action == "install":
329
+ result = install_skill(root, force=args.force, version=__version__)
330
+ elif args.action == "remove":
331
+ result = remove_skill(root, force=args.force)
332
+ else:
333
+ result = skill_status(root, version=__version__)
309
334
  if args.json:
310
335
  stdout.write(json.dumps({"ok": True, "mode": f"skill_{args.action}", **result}, indent=2) + "\n")
311
336
  elif args.action == "install":
312
337
  verb = "Installed" if result["created"] else "Updated" if result["updated"] else "Already installed"
313
338
  stdout.write(f"{verb} {result['path']}\n")
339
+ elif args.action == "status":
340
+ for key, value in result.items():
341
+ label = key.replace("_", " ").capitalize()
342
+ display = (
343
+ "not applicable" if value is None else str(value).lower() if isinstance(value, bool) else str(value)
344
+ )
345
+ stdout.write(f"{label}: {display}\n")
314
346
  elif result["removed"]:
315
347
  stdout.write(f"Removed {result['path']}\n")
316
348
  else:
@@ -55,7 +55,7 @@ def parse_inline(token: Token, *, line: int, input_path: str | None) -> list[Inl
55
55
  )
56
56
  elif child_type in {"link_open", "link_close"}:
57
57
  raise UnsupportedFeatureError(
58
- "Links require a public python-docx hyperlink creation API and are not supported in 0.1.0.",
58
+ "Links require a public python-docx hyperlink creation API and are not supported in 0.2.0.",
59
59
  line=line,
60
60
  input_path=input_path,
61
61
  )
@@ -264,7 +264,7 @@ def _consume_blockquote(tokens: list[Token], index: int, input_path: str) -> tup
264
264
  index += 1
265
265
  while index < len(tokens) and tokens[index].type != "blockquote_close":
266
266
  if tokens[index].type != "paragraph_open":
267
- _unsupported("Blockquotes may contain paragraphs only in 0.1.0.", tokens[index], input_path)
267
+ _unsupported("Blockquotes may contain paragraphs only in 0.2.0.", tokens[index], input_path)
268
268
  paragraph, index = _consume_paragraph(tokens, index, input_path)
269
269
  if any(fragment.kind == "image" for fragment in paragraph.fragments):
270
270
  _unsupported("Images nested in blockquotes are not supported.", opening, input_path)
@@ -299,7 +299,7 @@ def _consume_list(
299
299
  if start is not None and int(start) != 1:
300
300
  raise ParseError(
301
301
  "ordered_list_start_unsupported",
302
- "Ordered lists must begin with 1 in 0.1.0.",
302
+ "Ordered lists must begin with 1 in 0.2.0.",
303
303
  line=_token_line(opening),
304
304
  input_path=input_path,
305
305
  )