docmost-cli 0.4.0__tar.gz → 0.6.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 (135) hide show
  1. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/.claude/skills/docmost/SKILL.md +18 -4
  2. docmost_cli-0.6.0/.github/workflows/ci.yml +139 -0
  3. docmost_cli-0.6.0/.github/workflows/release.yml +224 -0
  4. docmost_cli-0.6.0/CHANGELOG.md +249 -0
  5. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/CLAUDE.md +53 -3
  6. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/PKG-INFO +43 -13
  7. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/README.md +42 -11
  8. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/SPECIFICATION.md +184 -47
  9. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-attachment.1 +37 -3
  10. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-comment.1 +37 -3
  11. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-config.1 +32 -2
  12. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-page.1 +141 -15
  13. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-search.1 +30 -5
  14. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-space.1 +37 -3
  15. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-sync.1 +22 -11
  16. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli-user.1 +4 -1
  17. docmost_cli-0.6.0/man/man1/docmost-cli-workspace.1 +105 -0
  18. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/man/man1/docmost-cli.1 +54 -3
  19. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/pyproject.toml +6 -2
  20. docmost_cli-0.6.0/release-notes.md +62 -0
  21. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/__init__.py +1 -1
  22. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/attachments.py +6 -2
  23. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/auth.py +22 -4
  24. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/client.py +6 -4
  25. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/comments.py +13 -3
  26. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/pages.py +271 -53
  27. docmost_cli-0.6.0/src/docmost_cli/api/pagination.py +298 -0
  28. docmost_cli-0.6.0/src/docmost_cli/api/position.py +287 -0
  29. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/spaces.py +23 -7
  30. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/users.py +4 -4
  31. docmost_cli-0.6.0/src/docmost_cli/cli/_list_opts.py +248 -0
  32. docmost_cli-0.6.0/src/docmost_cli/cli/attachment.py +53 -0
  33. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/comment.py +32 -8
  34. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/config_cmd.py +66 -14
  35. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/main.py +52 -3
  36. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/page.py +191 -32
  37. docmost_cli-0.6.0/src/docmost_cli/cli/search.py +56 -0
  38. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/space.py +30 -7
  39. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/sync_cmd.py +21 -2
  40. docmost_cli-0.6.0/src/docmost_cli/cli/user.py +30 -0
  41. docmost_cli-0.6.0/src/docmost_cli/cli/workspace.py +66 -0
  42. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/config/settings.py +3 -0
  43. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/config/store.py +17 -12
  44. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/convert/prosemirror_to_md.py +10 -2
  45. docmost_cli-0.6.0/src/docmost_cli/models/__init__.py +5 -0
  46. docmost_cli-0.6.0/src/docmost_cli/models/common.py +47 -0
  47. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/output/__init__.py +6 -0
  48. docmost_cli-0.6.0/src/docmost_cli/output/formatter.py +144 -0
  49. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/sync/diff.py +1 -1
  50. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/sync/manifest.py +8 -8
  51. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/sync/push.py +114 -53
  52. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_auth.py +54 -0
  53. docmost_cli-0.6.0/tests/test_api/test_pages.py +504 -0
  54. docmost_cli-0.6.0/tests/test_api/test_pagination.py +249 -0
  55. docmost_cli-0.6.0/tests/test_api/test_position.py +127 -0
  56. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_spaces.py +67 -0
  57. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_comment_commands.py +42 -0
  58. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_config_commands.py +31 -0
  59. docmost_cli-0.6.0/tests/test_cli/test_list_fields.py +139 -0
  60. docmost_cli-0.6.0/tests/test_cli/test_main.py +60 -0
  61. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_page_commands.py +222 -5
  62. docmost_cli-0.6.0/tests/test_cli/test_user_commands.py +55 -0
  63. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_workspace_commands.py +50 -10
  64. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_config/test_store.py +35 -0
  65. docmost_cli-0.6.0/tests/test_docs/test_man_pages.py +52 -0
  66. docmost_cli-0.6.0/tests/test_output/test_formatter.py +131 -0
  67. docmost_cli-0.6.0/tests/test_sync/__init__.py +0 -0
  68. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_sync/test_push.py +103 -92
  69. docmost_cli-0.4.0/CHANGELOG.md +0 -102
  70. docmost_cli-0.4.0/man/man1/docmost-cli-workspace.1 +0 -68
  71. docmost_cli-0.4.0/src/docmost_cli/api/pagination.py +0 -94
  72. docmost_cli-0.4.0/src/docmost_cli/cli/attachment.py +0 -30
  73. docmost_cli-0.4.0/src/docmost_cli/cli/search.py +0 -33
  74. docmost_cli-0.4.0/src/docmost_cli/cli/user.py +0 -25
  75. docmost_cli-0.4.0/src/docmost_cli/cli/workspace.py +0 -40
  76. docmost_cli-0.4.0/src/docmost_cli/models/__init__.py +0 -3
  77. docmost_cli-0.4.0/src/docmost_cli/models/common.py +0 -3
  78. docmost_cli-0.4.0/src/docmost_cli/output/formatter.py +0 -85
  79. docmost_cli-0.4.0/tests/test_api/test_pages.py +0 -289
  80. docmost_cli-0.4.0/tests/test_api/test_pagination.py +0 -62
  81. docmost_cli-0.4.0/tests/test_cli/test_user_commands.py +0 -28
  82. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/.claude/skills/docmost/examples.md +0 -0
  83. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/.gitignore +0 -0
  84. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/LICENSE +0 -0
  85. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/__main__.py +0 -0
  86. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/__init__.py +0 -0
  87. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/search.py +0 -0
  88. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/api/workspace.py +0 -0
  89. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/cli/__init__.py +0 -0
  90. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/config/__init__.py +0 -0
  91. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/convert/__init__.py +0 -0
  92. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/output/tree.py +0 -0
  93. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/py.typed +0 -0
  94. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/sync/__init__.py +0 -0
  95. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/sync/frontmatter.py +0 -0
  96. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/src/docmost_cli/sync/pull.py +0 -0
  97. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/__init__.py +0 -0
  98. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/conftest.py +0 -0
  99. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/code_block.json +0 -0
  100. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/code_block.md +0 -0
  101. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/empty_doc.json +0 -0
  102. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/empty_doc.md +0 -0
  103. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/headings_and_text.json +0 -0
  104. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/headings_and_text.md +0 -0
  105. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/lists.json +0 -0
  106. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/lists.md +0 -0
  107. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/marks.json +0 -0
  108. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/marks.md +0 -0
  109. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/simple_paragraph.json +0 -0
  110. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/simple_paragraph.md +0 -0
  111. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/table.json +0 -0
  112. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/fixtures/table.md +0 -0
  113. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/__init__.py +0 -0
  114. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_attachments.py +0 -0
  115. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_client.py +0 -0
  116. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_comments.py +0 -0
  117. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_search.py +0 -0
  118. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_users.py +0 -0
  119. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_api/test_workspace.py +0 -0
  120. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/__init__.py +0 -0
  121. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_attachment_commands.py +0 -0
  122. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_search_commands.py +0 -0
  123. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_space_commands.py +0 -0
  124. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_cli/test_sync_commands.py +0 -0
  125. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_config/__init__.py +0 -0
  126. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_config/test_settings.py +0 -0
  127. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_convert/__init__.py +0 -0
  128. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_convert/test_prosemirror_to_md.py +0 -0
  129. {docmost_cli-0.4.0/tests/test_output → docmost_cli-0.6.0/tests/test_docs}/__init__.py +0 -0
  130. {docmost_cli-0.4.0/tests/test_sync → docmost_cli-0.6.0/tests/test_output}/__init__.py +0 -0
  131. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_output/test_tree.py +0 -0
  132. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_sync/test_diff.py +0 -0
  133. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_sync/test_frontmatter.py +0 -0
  134. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_sync/test_manifest.py +0 -0
  135. {docmost_cli-0.4.0 → docmost_cli-0.6.0}/tests/test_sync/test_pull.py +0 -0
@@ -96,19 +96,33 @@ docmost-cli page export <page-id> --format md
96
96
 
97
97
  1. Get current content: `docmost-cli page get <page-id>`
98
98
  2. Modify the Markdown content as needed
99
- 3. Update: `docmost-cli page update <page-id> --title "Updated Title"`
100
- 4. Note: Content update via REST may not be available on Community edition
99
+ 3. Update: `docmost-cli page update <page-id> --file updated.md`
100
+ 4. Content updates are applied in place the page keeps its ID, slug, history
101
+ and inbound links. Works on Community and Enterprise alike, from Docmost
102
+ v0.71. Use `--append` / `--prepend` to add without replacing.
101
103
 
102
104
  ## Output Format
103
105
 
104
106
  - **Content commands** (`page get`): Raw Markdown to stdout — directly usable
105
- - **List commands** (`--json`): JSON array parse with standard JSON tools
107
+ - **List commands** (`--json`): JSON array of **complete server objects** every
108
+ field the API returned, not a subset. For pages that means `id`, `slugId`,
109
+ `title`, `icon`, `position`, `parentPageId`, `spaceId`, `creatorId`,
110
+ `lastUpdatedById`, `isLocked`, `createdAt`, `updatedAt` and more. Page
111
+ *content* is not included; fetch it with `page get <id>`.
112
+ Pagination is followed automatically, so the array is complete; add
113
+ `--envelope` if you need `hasNextPage` / `nextCursor`
114
+ - **Narrow the output** with `--fields id,title,updatedAt` when you only need a
115
+ few keys — worth doing to keep context small, since `--json` is now much
116
+ larger. It works in JSON and table mode, and an unknown name errors with the
117
+ list of fields the server actually returned
118
+ - **Single-item commands** (`user me`, `workspace info`): add `--json` for the
119
+ complete object
106
120
  - **Write commands** (`page create`, `delete`): Page ID to stdout, confirmation to stderr
107
121
  - **Always use `--json`** for list/search commands when you need to parse the output programmatically
108
122
 
109
123
  ## Tips
110
124
 
111
- - Use `--json` flag on list commands to get parseable output
125
+ - Use `--json` on list commands to get parseable output, plus `--fields` to keep it small
112
126
  - Page IDs are UUIDs like `019a2a69-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
113
127
  - Space slugs are lowercase alphanumeric (e.g., `engineering`, `devtest`)
114
128
  - Use `-y` flag to skip confirmation prompts for automated workflows
@@ -0,0 +1,139 @@
1
+ name: CI
2
+
3
+ # Uses only first-party actions, and installs with pip rather than uv. The
4
+ # project prefers uv locally (see CLAUDE.md), but this runner could not resolve
5
+ # `astral-sh/setup-uv@v9` at all, and pip is the documented fallback — a CI file
6
+ # that reliably runs beats a marginally faster one.
7
+ #
8
+ # Pins are the current majors, which target Node 24. Node 20 is deprecated and
9
+ # is being force-migrated on the runners, so checkout@v4 / setup-python@v5 would
10
+ # have had a shelf life.
11
+
12
+ on:
13
+ push:
14
+ branches: [main]
15
+ pull_request:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ concurrency:
21
+ group: ${{ github.workflow }}-${{ github.ref }}
22
+ cancel-in-progress: true
23
+
24
+ jobs:
25
+ test:
26
+ name: test (py${{ matrix.python-version }})
27
+ runs-on: ubuntu-latest
28
+ strategy:
29
+ # Report every interpreter, rather than hiding 3.13 behind a 3.11 failure.
30
+ fail-fast: false
31
+ matrix:
32
+ python-version: ["3.11", "3.12", "3.13"]
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+
36
+ - uses: actions/setup-python@v6
37
+ with:
38
+ python-version: ${{ matrix.python-version }}
39
+ cache: pip
40
+ cache-dependency-path: pyproject.toml
41
+
42
+ - name: Install
43
+ run: |
44
+ python -m pip install --upgrade pip
45
+ python -m pip install -e ".[dev]"
46
+
47
+ - name: Verify the interpreter matches the matrix
48
+ # Without this, a misconfigured setup step would silently run every
49
+ # matrix entry on the runner's default Python and report three passes
50
+ # for one version.
51
+ run: |
52
+ python - <<'PY'
53
+ import sys
54
+ actual = "{}.{}".format(*sys.version_info[:2])
55
+ expected = "${{ matrix.python-version }}"
56
+ assert actual == expected, f"expected Python {expected}, got {actual}"
57
+ print("Python", sys.version)
58
+ PY
59
+
60
+ - name: Test
61
+ run: python -m pytest tests/ -q
62
+
63
+ quality:
64
+ name: lint and type-check
65
+ runs-on: ubuntu-latest
66
+ steps:
67
+ - uses: actions/checkout@v7
68
+
69
+ - uses: actions/setup-python@v6
70
+ with:
71
+ python-version: "3.11"
72
+ cache: pip
73
+ cache-dependency-path: pyproject.toml
74
+
75
+ - name: Install
76
+ run: |
77
+ python -m pip install --upgrade pip
78
+ python -m pip install -e ".[dev]"
79
+
80
+ - name: Ruff lint
81
+ run: python -m ruff check src/ tests/
82
+
83
+ - name: Ruff format
84
+ run: python -m ruff format --check src/ tests/
85
+
86
+ # Runs once, not per matrix entry: pyproject pins mypy's
87
+ # python_version = "3.11", so the result is interpreter-independent.
88
+ - name: Mypy (strict)
89
+ run: python -m mypy src/
90
+
91
+ build:
92
+ name: build
93
+ runs-on: ubuntu-latest
94
+ steps:
95
+ - uses: actions/checkout@v7
96
+
97
+ - uses: actions/setup-python@v6
98
+ with:
99
+ python-version: "3.11"
100
+
101
+ - name: Build sdist and wheel
102
+ run: |
103
+ python -m pip install --upgrade pip build
104
+ python -m build
105
+
106
+ - name: Verify the built version matches __version__
107
+ # The version is dynamic: hatch reads it out of src/docmost_cli/__init__.py.
108
+ # If that regex ever stops matching, the build silently produces the wrong
109
+ # version rather than failing.
110
+ run: |
111
+ python - <<'PY'
112
+ import pathlib, re
113
+
114
+ source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
115
+ match = re.search(r'__version__ = "([^"]+)"', source)
116
+ assert match, "no __version__ in src/docmost_cli/__init__.py"
117
+ expected = match.group(1)
118
+
119
+ built = sorted(p.name for p in pathlib.Path("dist").glob("*.whl"))
120
+ assert built, "no wheel produced"
121
+ assert any(f"-{expected}-" in name for name in built), (
122
+ f"__version__ is {expected} but built {built}"
123
+ )
124
+ print(f"version {expected} OK: {built}")
125
+ PY
126
+
127
+ - name: Verify the man pages ship in the wheel
128
+ # They are installed via [tool.hatch.build.targets.wheel.shared-data],
129
+ # which breaks quietly if the path ever moves.
130
+ run: |
131
+ python - <<'PY'
132
+ import pathlib, zipfile
133
+
134
+ wheel = next(iter(pathlib.Path("dist").glob("*.whl")))
135
+ names = zipfile.ZipFile(wheel).namelist()
136
+ pages = [n for n in names if n.endswith(".1") and "share/man/man1" in n]
137
+ assert len(pages) == 10, f"expected 10 man pages in the wheel, found {len(pages)}"
138
+ print(f"{len(pages)} man pages OK")
139
+ PY
@@ -0,0 +1,224 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI and cuts a GitHub Release when a v* tag is pushed.
4
+ #
5
+ # Authentication is PyPI Trusted Publishing (OIDC) — no API token is stored in
6
+ # this repository. One-time setup on pypi.org, under the project's
7
+ # "Publishing" settings, add a trusted publisher:
8
+ #
9
+ # Owner: glinhard
10
+ # Repository: docmost-cli
11
+ # Workflow name: release.yml
12
+ # Environment: pypi
13
+ #
14
+ # Dry runs: trigger this workflow manually (Actions -> Release -> Run workflow)
15
+ # to exercise the whole chain — version guard, build, artifact checks, and
16
+ # action resolution — without uploading anything anywhere.
17
+
18
+ on:
19
+ push:
20
+ tags: ["v*"]
21
+ workflow_dispatch:
22
+ inputs:
23
+ dry-run:
24
+ description: "Build and verify only; do not publish"
25
+ type: boolean
26
+ default: true
27
+
28
+ permissions:
29
+ contents: read
30
+
31
+ jobs:
32
+ verify:
33
+ name: verify and build
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v7
37
+
38
+ - uses: actions/setup-python@v6
39
+ with:
40
+ python-version: "3.11"
41
+ cache: pip
42
+ cache-dependency-path: pyproject.toml
43
+
44
+ - name: Install
45
+ run: |
46
+ python -m pip install --upgrade pip build twine
47
+ python -m pip install -e ".[dev]"
48
+
49
+ - name: Full test suite
50
+ # The tag is the last chance to catch a regression before it is
51
+ # published immutably: PyPI does not allow re-uploading a version.
52
+ run: python -m pytest tests/ -q
53
+
54
+ - name: Verify the tag matches __version__
55
+ if: startsWith(github.ref, 'refs/tags/')
56
+ run: |
57
+ python - <<'PY'
58
+ import os, pathlib, re
59
+
60
+ source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
61
+ version = re.search(r'__version__ = "([^"]+)"', source).group(1)
62
+ tag = os.environ["GITHUB_REF_NAME"]
63
+ assert tag == f"v{version}", f"tag {tag} does not match __version__ {version}"
64
+ print(f"tag {tag} matches __version__ {version}")
65
+ PY
66
+
67
+ - name: Verify this version is not already on PyPI
68
+ # PyPI rejects a duplicate upload anyway; failing here gives a clearer
69
+ # message than a 400 from the publish step.
70
+ run: |
71
+ python - <<'PY'
72
+ import json, pathlib, re, urllib.request
73
+
74
+ source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
75
+ version = re.search(r'__version__ = "([^"]+)"', source).group(1)
76
+ try:
77
+ with urllib.request.urlopen(
78
+ "https://pypi.org/pypi/docmost-cli/json", timeout=30
79
+ ) as response:
80
+ released = set(json.load(response)["releases"])
81
+ except Exception as exc: # noqa: BLE001 - never block a release on this
82
+ print(f"could not reach PyPI ({exc}); skipping the duplicate check")
83
+ else:
84
+ assert version not in released, (
85
+ f"{version} is already on PyPI; bump __version__ before tagging"
86
+ )
87
+ print(f"{version} is not yet on PyPI (has {sorted(released)})")
88
+ PY
89
+
90
+ - name: Build sdist and wheel
91
+ run: python -m build
92
+
93
+ - name: Check the distributions
94
+ run: python -m twine check dist/*
95
+
96
+ - name: Verify the wheel contents
97
+ run: |
98
+ python - <<'PY'
99
+ import pathlib, re, zipfile
100
+
101
+ source = pathlib.Path("src/docmost_cli/__init__.py").read_text()
102
+ version = re.search(r'__version__ = "([^"]+)"', source).group(1)
103
+
104
+ wheel = next(iter(pathlib.Path("dist").glob("*.whl")))
105
+ assert f"-{version}-" in wheel.name, f"built {wheel.name}, expected {version}"
106
+
107
+ names = zipfile.ZipFile(wheel).namelist()
108
+ pages = [n for n in names if n.endswith(".1") and "share/man/man1" in n]
109
+ assert len(pages) == 10, f"expected 10 man pages, found {len(pages)}"
110
+
111
+ metadata = next(n for n in names if n.endswith("METADATA"))
112
+ text = zipfile.ZipFile(wheel).read(metadata).decode()
113
+ assert "License-Expression: AGPL-3.0-or-later" in text, "missing licence expression"
114
+ assert "Classifier: License ::" not in text, (
115
+ "deprecated License :: classifier is back; PEP 639 replaces it "
116
+ "with the license expression"
117
+ )
118
+ print(f"{wheel.name}: {len(pages)} man pages, licence metadata OK")
119
+ PY
120
+
121
+ # upload@v7 and download@v8 are a matched pair: v7 uploads files directly
122
+ # instead of zipping them, and v8 is the major that stopped trying to
123
+ # un-zip a download that was never a zip. Bump them together.
124
+ - uses: actions/upload-artifact@v7
125
+ with:
126
+ name: dist
127
+ path: dist/
128
+
129
+ publish:
130
+ name: publish to PyPI
131
+ needs: verify
132
+ runs-on: ubuntu-latest
133
+ # The job itself also runs on a manual dispatch, so a dry run resolves and
134
+ # downloads every action here — including the third-party publish action.
135
+ # That matters: `astral-sh/setup-uv` turned out not to resolve on these
136
+ # runners at all, and a release is the worst moment to discover that.
137
+ environment:
138
+ name: pypi
139
+ url: https://pypi.org/project/docmost-cli/
140
+ permissions:
141
+ id-token: write # required for Trusted Publishing
142
+ steps:
143
+ - uses: actions/download-artifact@v8
144
+ with:
145
+ name: dist
146
+ path: dist/
147
+
148
+ - name: Publish
149
+ # Uploading is gated on the ref being a tag, not on the dry-run input.
150
+ # A dispatch always runs against a branch, so no manual invocation can
151
+ # publish, whatever inputs it is given.
152
+ if: startsWith(github.ref, 'refs/tags/')
153
+ uses: pypa/gh-action-pypi-publish@release/v1
154
+
155
+ - name: Dry run summary
156
+ if: ${{ !startsWith(github.ref, 'refs/tags/') }}
157
+ run: |
158
+ echo "Dry run: actions resolved and artifacts built, nothing published."
159
+ echo "Push a v* tag to publish for real."
160
+ ls -l dist/
161
+
162
+ github-release:
163
+ name: GitHub Release
164
+ needs: publish
165
+ runs-on: ubuntu-latest
166
+ if: startsWith(github.ref, 'refs/tags/')
167
+ permissions:
168
+ contents: write # required to create the release
169
+ steps:
170
+ - uses: actions/checkout@v7
171
+
172
+ - uses: actions/download-artifact@v8
173
+ with:
174
+ name: dist
175
+ path: dist/
176
+
177
+ - name: Extract this version's CHANGELOG section
178
+ run: |
179
+ python - <<'PY'
180
+ import os, pathlib, re
181
+
182
+ version = os.environ["GITHUB_REF_NAME"].removeprefix("v")
183
+ changelog = pathlib.Path("CHANGELOG.md").read_text()
184
+ # Everything from this version's heading up to the next "## " heading.
185
+ match = re.search(
186
+ rf"^## {re.escape(version)}.*?$(.*?)(?=^## )",
187
+ changelog,
188
+ re.MULTILINE | re.DOTALL,
189
+ )
190
+ body = match.group(1).strip() if match else f"See CHANGELOG.md for {version}."
191
+ pathlib.Path("release-notes.md").write_text(body + "\n")
192
+ print(body[:500])
193
+ PY
194
+
195
+ - name: Create or update the release
196
+ # The tag can arrive two ways: `git push origin v0.6.0`, or the GitHub
197
+ # web UI's "Draft a new release" flow, which creates the tag *and* the
198
+ # release before this job runs. Handle both, so neither route ends in a
199
+ # red job.
200
+ env:
201
+ GH_TOKEN: ${{ github.token }}
202
+ run: |
203
+ set -euo pipefail
204
+ tag="$GITHUB_REF_NAME"
205
+
206
+ if gh release view "$tag" >/dev/null 2>&1; then
207
+ echo "Release $tag already exists; attaching artifacts."
208
+ gh release upload "$tag" dist/* --clobber
209
+
210
+ # Only fill in notes if whoever created it left them empty —
211
+ # never overwrite notes a human wrote by hand.
212
+ if [ -z "$(gh release view "$tag" --json body --jq '.body' | tr -d '[:space:]')" ]; then
213
+ echo "Release body is empty; filling it from CHANGELOG.md."
214
+ gh release edit "$tag" --notes-file release-notes.md
215
+ else
216
+ echo "Release already has notes; leaving them alone."
217
+ fi
218
+ else
219
+ echo "Creating release $tag."
220
+ gh release create "$tag" \
221
+ --title "$tag" \
222
+ --notes-file release-notes.md \
223
+ dist/*
224
+ fi
@@ -0,0 +1,249 @@
1
+ # Changelog
2
+
3
+ ## 0.6.0 (2026-07-27)
4
+
5
+ ### Behavior changes
6
+
7
+ - **`--json` on list commands is now lossless.** It used to project each item
8
+ onto the same hardcoded column list the Rich table uses, silently discarding
9
+ every other field the server returned — `page list --json` dropped `slugId`,
10
+ `spaceId`, `creatorId`, `lastUpdatedById`, `position`, `isLocked`,
11
+ `createdAt` and `contributorIds`. Each element is now the complete server
12
+ object. The table is unchanged: its column list was always a display choice.
13
+ Page *content* is not part of the list payload (Docmost omits it from
14
+ `baseFields`), so listings stay small.
15
+ - **`--json` no longer invents keys.** A column the server omitted used to be
16
+ emitted as `null`; a missing field is now simply absent. An explicitly named
17
+ `--fields` entry still yields `null`, which keeps projections rectangular.
18
+ - `page list --tree --json` and flat `--json` now agree on shape — the tree path
19
+ already emitted complete objects.
20
+ - `page list --tree` now actually rejects `--page-size`, which its own error
21
+ message has claimed since 0.5.0 while never checking it.
22
+ - Table cells render `None` as blank instead of the literal string `None`
23
+ (visible in `page list`'s `parentPageId` column for every root page).
24
+ - `config show --json` emits `no_session_cache` as a real JSON boolean. The
25
+ table still shows it.
26
+
27
+ ### Added
28
+
29
+ - `--fields a,b,c` on all eight list commands: projects the JSON output *and*
30
+ replaces the table's columns, so the table is configurable for the first time.
31
+ Names are validated against the fields the server actually returned — an
32
+ unknown name is a usage error listing what is available, because a typo would
33
+ otherwise yield a silent full-length column of nulls.
34
+ - `--fields` under `page list --tree --json`, projecting every node recursively
35
+ while always preserving the nested `children` array. It requires `--json`,
36
+ since the indented tree view has no columns to replace.
37
+ - `--json` on `workspace info`, `user me` and `config show`, which previously had
38
+ no machine-readable output at all. The first two emit the complete server
39
+ object and also accept `--fields`. `config show --json` masks `api_key` and
40
+ `password` exactly as the table does.
41
+ - `print_json()` in `output/`, now the single JSON writer — `page list --tree
42
+ --json` and `page get --raw` route through it, which also gives `--raw` the
43
+ `default=str` fallback it was missing.
44
+ - `emit_item()` in `cli/_list_opts.py`, the single-item mirror of `emit_list()`.
45
+
46
+ ### Fixed
47
+
48
+ - `mypy src/` is clean under `strict = true`; it had been carrying 23 errors.
49
+ Two were real defects rather than annotation noise: `extract_id()` was declared
50
+ `-> str` but returned whatever the server put in `"id"`, so a numeric id would
51
+ have propagated a non-string page ID into request bodies; and `_node_heading()`
52
+ computed `"#" * level` from an unvalidated nested `.get()`, so a ProseMirror
53
+ document with a string or null heading level raised `TypeError` mid-render.
54
+ - `config show` masking is now produced in exactly one place, so no future
55
+ renderer can add a path that forgets to mask.
56
+
57
+ ### Changed
58
+
59
+ - `print_table()` gained a keyword-only `fields` argument; `columns` no longer
60
+ affects JSON output.
61
+ - Corrected `SPECIFICATION.md` §7.2, whose sample payload showed a projected
62
+ three-key object and documented a field named `updated` that has never existed
63
+ (the code emits `updatedAt`), and its stale `print_table` signature.
64
+ - Removed two README rows for things that were never implemented: a
65
+ `space get` command and a `space list --detail` flag. Corrected `search` to
66
+ `search query`.
67
+
68
+ ## 0.5.0 (2026-07-26)
69
+
70
+ Fixes six gaps reported against Docmost Community 0.95, verified against the
71
+ upstream v0.95.0 server source.
72
+
73
+ ### Behavior changes
74
+
75
+ - **List commands now return every result.** Pagination is followed
76
+ automatically instead of stopping at the server's first page. `--limit` is now
77
+ a cap on the *total* across pages rather than a per-request limit, so
78
+ `--limit 200` fetches two pages of 100 instead of failing with HTTP 400.
79
+ - **`sync push` no longer recreates pages silently.** On a server too old to
80
+ update content in place it aborts with an explanation; pass `--allow-recreate`
81
+ to opt into the old delete+recreate behavior, which prints a warning naming the
82
+ affected page count.
83
+ - **`page move --position` takes `first`, `last`, or an ordering key** instead of
84
+ an integer, and defaults to `first`. `POSITION_FIRST` (`"aaaaa"`) is gone — it
85
+ never meant "first" anyway: in ASCII `'a' > '0'`, so it sorted *after* every key
86
+ Docmost's own generator produces.
87
+ - `move_page()`'s `position` argument is now required (the server requires it).
88
+
89
+ ### Fixed
90
+
91
+ - **Pagination metadata was silently discarded.** `get_cursor()` looked for
92
+ `data.cursor`, but Docmost returns `data.meta.nextCursor`, so it returned `None`
93
+ every time — which is why the auto-follow documented since 0.2.0 never actually
94
+ ran and `--cursor` was a write-only flag.
95
+ - **`page children` capped at 20.** It sent no `limit` or `cursor` to
96
+ `/pages/sidebar-pages` and got the server default.
97
+ - **`page move --parent` returned HTTP 400 without `--position`.** Docmost's
98
+ `MovePageDto` declares `position` as required (`@MinLength(5) @MaxLength(12)`);
99
+ the CLI dropped it from the body when unset.
100
+ - **Content updates failed with a misleading message.** The CLI POSTed to
101
+ `/pages/content/update`, an endpoint that does not exist in Docmost, and read
102
+ the 404 as an Enterprise-only restriction. Content now goes through
103
+ `POST /pages/update`, which Docmost applies server-side through its
104
+ collaboration gateway. The page keeps its ID, slug, history, comments and
105
+ inbound links — no WebSocket or Yjs client needed. Requires Docmost v0.71+.
106
+ - **Space slug resolution failed past the first page of spaces**, reporting a
107
+ misleading `Space '<slug>' not found.` Now follows pagination, stopping at the
108
+ first match.
109
+ - **`sync pull` fetched an incomplete tree** on spaces with more root pages or
110
+ children than one server page, silently producing a partial local copy.
111
+ - **`sync push` lost the manifest when a phase aborted**, so the next push
112
+ re-created every already-created page as a duplicate. The manifest is now saved
113
+ in a `finally` block.
114
+ - `update_page_content` only intercepted HTTP 404; a 405 or 403 fell through to a
115
+ bare "Unexpected error".
116
+
117
+ ### Added
118
+
119
+ - `--version` / `-V` and a `version` subcommand. With `--verbose`, `version` also
120
+ reports the Python version and platform. The version now has a single source of
121
+ truth (`src/docmost_cli/__init__.py`), with `pyproject.toml` deriving it.
122
+ - `--no-session-cache` global flag, `DOCMOST_NO_SESSION_CACHE` environment
123
+ variable and `no_session_cache` config key: the session JWT stays in memory and
124
+ is never read from or written to `~/.cache/docmost-cli/session.json`.
125
+ - `docmost-cli config logout` deletes an existing cached session token.
126
+ - `--limit`, `--page-size`, `--cursor`, `--no-follow` and `--envelope` on all
127
+ eight list commands. `--json` still emits a bare array; `--envelope` wraps it as
128
+ `{"items": [...], "meta": {...}}` carrying `hasNextPage` and `nextCursor`.
129
+ - `page update --append` / `--prepend`.
130
+ - `page move --root` to move a nested page to the space root.
131
+ - `page list --tree --json` outputs the nested tree as JSON (it was previously
132
+ ignored).
133
+ - `api/position.py`: base62 fractional indexing ported from the reference
134
+ algorithm, with jitter to satisfy Docmost's 5-character minimum. Keys stay valid
135
+ for Docmost's own JS generator, so dragging a sibling next to a CLI-placed page
136
+ in the web editor still works.
137
+ - `models/common.py`: `PaginationMeta` and `PaginatedResult` pydantic models.
138
+ - Guards against a server that ignores `cursor`: pagination stops when a cursor or
139
+ a page repeats, and warns, rather than looping forever.
140
+
141
+ ### Changed
142
+
143
+ - `--tree` now rejects the pagination flags instead of silently ignoring them.
144
+ - `print_table()` gained a keyword-only `meta` argument; `print_table` and the new
145
+ `print_warning` are exported from `docmost_cli.output`.
146
+ - Corrected `SPECIFICATION.md` §5.2/§5.3 (response envelope shape, `/pages/move`
147
+ requirements, the non-existent content endpoint) and the `--position` docs in
148
+ the man pages. A new doc test keeps the man-page `.TH` versions in sync.
149
+
150
+ ## 0.4.0 (2026-03-22)
151
+
152
+ - Add `sync pull` command: download all pages from a space to local Markdown files with YAML frontmatter
153
+ - Add `sync push` command: upload local changes to server (create, update, move pages)
154
+ - Add `sync status` command: show changes between local files and last-pulled state
155
+ - Edition-aware content updates: Enterprise uses REST content update (preserves page ID), Community uses safe create-then-delete (new page created and verified before old page removed)
156
+ - Flat directory layout with `.docmost-manifest.json` tracking sync state and SHA-256 content hashes
157
+ - `--dry-run` flag on push to preview changes without executing
158
+ - `--delete` flag on push to remove server pages not found locally (opt-in safety)
159
+ - `--force` flag on pull to overwrite existing synced data
160
+ - Topological sort ensures parent pages are created before children
161
+ - ID remapping: when Community edition forces new page IDs, frontmatter and manifest are updated automatically
162
+ - 105 new tests (96 sync module + 9 CLI integration)
163
+
164
+ ## 0.3.1 (2026-03-22)
165
+
166
+ - Fix `page list --tree` and `page children` on Community edition: use `/pages/sidebar-pages` instead of `/pages/children` (404 on v0.70.3)
167
+ - Tree recursion: only fetch children for pages with `hasChildren: true` (eliminates ~40 wasted API calls on a 50-page space)
168
+ - Narrow error handling in tree builder (auth failures no longer silently swallowed)
169
+ - Reduce API calls in `page get` (fetch page info once, not twice)
170
+ - Remove raw HTTP call from CLI layer (use API function instead)
171
+ - Deduplicate UTF-8 stdio reconfigure into `_ensure_utf8_stdio()`
172
+ - Use `build_body` helper consistently in all API functions
173
+ - Remove dead code: unused models, unused parameter
174
+
175
+ ## 0.3.0 (2026-03-22)
176
+
177
+ - Add `parentPageId` to `page list --json` output columns
178
+ - Add `parent_id` to `page get --meta` YAML frontmatter
179
+ - Add `--icon` flag to `page update` command
180
+ - Add recursive tree fallback: `page list --tree` fills in missing children
181
+ - Add "See also" cross-references to all page command help text
182
+ - Fix `page children` and `--tree` on Community edition: use `/pages/sidebar-pages` instead of `/pages/children` (404 on v0.70.3)
183
+ - Fix emoji/Unicode crash: move UTF-8 encoding to correct entry point
184
+ - Fix `--parent` on `page create`: use fractional index position string
185
+ - Fix `--content` escape sequences: `\n` and `\t` now work as newline/tab
186
+
187
+ ## 0.2.4 (2026-03-22)
188
+
189
+ - Fix emoji/Unicode crash for real: move UTF-8 reconfigure from `__main__.py` to `cli/main.py` (the actual entry point used by `docmost-cli` script bypasses `__main__.py`)
190
+
191
+ ## 0.2.3 (2026-03-22)
192
+
193
+ - Fix `--parent` on `page create`: send fractional index position string (Docmost requires 5-12 char string, not integer)
194
+ - Fix emoji crash on all Windows terminals: remove `isatty()` guard, always reconfigure to UTF-8 on Windows
195
+ - Fix `--content` escape sequences: `\n` and `\t` now interpreted as actual newline/tab
196
+ - Position parameter on `page move` changed from `int` to `str` (fractional index format)
197
+
198
+ ## 0.2.2 (2026-03-22)
199
+
200
+ - Fix `--parent` on `page create` silently ignored (import endpoint ignores parentPageId; now calls move_page as fallback)
201
+ - Fix `--help` crash on Windows (`OSError` from Rich's LegacyWindowsRenderer on cp1252 consoles)
202
+ - Fix `page get --meta` crash when page content contains emoji (✅❌⚠️📊)
203
+ - Reconfigure stdout/stderr to UTF-8 at startup on Windows interactive terminals
204
+
205
+ ## 0.2.1 (2026-03-22)
206
+
207
+ - Fix tree view crash on Windows with emoji page icons (cp1252 encoding)
208
+ - Fix silent Enterprise endpoint probe leaking error messages on Community edition
209
+ - Fix API endpoints discovered during live integration testing:
210
+ - `/spaces/list` → `/spaces`
211
+ - `/comments/list` → `/comments`
212
+ - `/pages/export` format `md` → `markdown`, response is ZIP not JSON
213
+ - Auth token extracted from `authToken` cookie (not `token`)
214
+ - Comment content JSON-stringified for API
215
+ - Consolidate duplicated code: shared `extract_items`, `extract_id`, `build_body` helpers
216
+ - Remove 6 dead stub files (-84 lines)
217
+ - Add `post_raw()` to DocmostClient for binary/probe responses
218
+ - Fix double file read in page import
219
+ - Add Claude Code skill (`/docmost`) for wiki interaction
220
+ - Prepare for PyPI: py.typed marker, CHANGELOG, dependency upper bounds, classifiers
221
+
222
+ ## 0.2.0 (2026-03-22)
223
+
224
+ - Retry with exponential backoff for transient errors (429, 5xx)
225
+ - `--verbose` HTTP debug logging (request/response to stderr)
226
+ - Page duplicate, copy, children, history, export, import commands
227
+ - Tree view (`--tree`) for page listing
228
+ - Workspace info/members, user me, attachment search commands
229
+ - Pagination auto-follow with safety guard (max 1000 iterations)
230
+ - ProseMirror-to-Markdown converter (all block nodes and marks)
231
+ - Claude Code skill (`/docmost`) for wiki interaction
232
+ - Comprehensive README with command reference
233
+ - MIT LICENSE file
234
+ - Session cache file permissions (0600)
235
+ - 175 tests, 0 lint errors
236
+
237
+ ## 0.1.0 (2026-03-22)
238
+
239
+ - Initial release
240
+ - Project scaffolding with typer CLI framework
241
+ - Configuration system with TOML profiles and environment variable overrides
242
+ - HTTP client with API key (Enterprise) and session (Community) auth auto-detection
243
+ - Page CRUD: create (via import endpoint), read, update, delete, move
244
+ - Space list, create, update
245
+ - Comment list, create, update (ProseMirror JSON wrapping)
246
+ - Full-text search with space filtering
247
+ - Output helpers enforcing stdout/stderr separation
248
+ - Edition-agnostic design (Community + Enterprise)
249
+ - 50 tests with pytest + pytest-httpx