docmost-cli 0.6.0__tar.gz → 0.7.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 (125) hide show
  1. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/.github/workflows/ci.yml +31 -0
  2. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/.github/workflows/release.yml +11 -4
  3. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/CHANGELOG.md +122 -0
  4. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/CLAUDE.md +4 -1
  5. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/PKG-INFO +3 -2
  6. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/README.md +2 -1
  7. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/SPECIFICATION.md +25 -4
  8. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-attachment.1 +67 -1
  9. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-comment.1 +1 -1
  10. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-config.1 +1 -1
  11. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-page.1 +25 -1
  12. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-search.1 +1 -1
  13. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-space.1 +1 -1
  14. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-sync.1 +1 -1
  15. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-user.1 +1 -1
  16. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli-workspace.1 +1 -1
  17. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/man/man1/docmost-cli.1 +6 -5
  18. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/__init__.py +1 -1
  19. docmost_cli-0.7.0/src/docmost_cli/api/attachments.py +97 -0
  20. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/client.py +34 -21
  21. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/pages.py +13 -13
  22. docmost_cli-0.7.0/src/docmost_cli/cli/attachment.py +98 -0
  23. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/config_cmd.py +20 -20
  24. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/main.py +20 -31
  25. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/page.py +26 -16
  26. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/sync_cmd.py +12 -25
  27. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/output/__init__.py +4 -0
  28. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/output/formatter.py +31 -9
  29. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/output/tree.py +2 -3
  30. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/__init__.py +4 -0
  31. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/diff.py +41 -2
  32. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/manifest.py +22 -0
  33. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/pull.py +17 -19
  34. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/push.py +48 -61
  35. docmost_cli-0.7.0/tests/test_api/test_attachments.py +122 -0
  36. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_client.py +74 -0
  37. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_attachment_commands.py +48 -0
  38. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_page_commands.py +129 -11
  39. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_sync_commands.py +69 -0
  40. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_docs/test_man_pages.py +36 -0
  41. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_output/test_formatter.py +18 -1
  42. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_sync/test_diff.py +90 -1
  43. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_sync/test_push.py +26 -3
  44. docmost_cli-0.6.0/src/docmost_cli/api/attachments.py +0 -34
  45. docmost_cli-0.6.0/src/docmost_cli/cli/attachment.py +0 -53
  46. docmost_cli-0.6.0/tests/test_api/test_attachments.py +0 -45
  47. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/.claude/skills/docmost/SKILL.md +0 -0
  48. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/.claude/skills/docmost/examples.md +0 -0
  49. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/.gitignore +0 -0
  50. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/LICENSE +0 -0
  51. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/pyproject.toml +0 -0
  52. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/release-notes.md +0 -0
  53. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/__main__.py +0 -0
  54. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/__init__.py +0 -0
  55. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/auth.py +0 -0
  56. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/comments.py +0 -0
  57. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/pagination.py +0 -0
  58. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/position.py +0 -0
  59. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/search.py +0 -0
  60. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/spaces.py +0 -0
  61. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/users.py +0 -0
  62. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/api/workspace.py +0 -0
  63. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/__init__.py +0 -0
  64. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/_list_opts.py +0 -0
  65. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/comment.py +0 -0
  66. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/search.py +0 -0
  67. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/space.py +0 -0
  68. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/user.py +0 -0
  69. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/cli/workspace.py +0 -0
  70. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/config/__init__.py +0 -0
  71. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/config/settings.py +0 -0
  72. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/config/store.py +0 -0
  73. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/convert/__init__.py +0 -0
  74. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/convert/prosemirror_to_md.py +0 -0
  75. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/models/__init__.py +0 -0
  76. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/models/common.py +0 -0
  77. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/py.typed +0 -0
  78. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/src/docmost_cli/sync/frontmatter.py +0 -0
  79. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/__init__.py +0 -0
  80. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/conftest.py +0 -0
  81. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/code_block.json +0 -0
  82. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/code_block.md +0 -0
  83. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/empty_doc.json +0 -0
  84. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/empty_doc.md +0 -0
  85. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/headings_and_text.json +0 -0
  86. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/headings_and_text.md +0 -0
  87. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/lists.json +0 -0
  88. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/lists.md +0 -0
  89. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/marks.json +0 -0
  90. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/marks.md +0 -0
  91. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/simple_paragraph.json +0 -0
  92. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/simple_paragraph.md +0 -0
  93. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/table.json +0 -0
  94. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/fixtures/table.md +0 -0
  95. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/__init__.py +0 -0
  96. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_auth.py +0 -0
  97. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_comments.py +0 -0
  98. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_pages.py +0 -0
  99. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_pagination.py +0 -0
  100. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_position.py +0 -0
  101. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_search.py +0 -0
  102. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_spaces.py +0 -0
  103. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_users.py +0 -0
  104. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_api/test_workspace.py +0 -0
  105. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/__init__.py +0 -0
  106. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_comment_commands.py +0 -0
  107. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_config_commands.py +0 -0
  108. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_list_fields.py +0 -0
  109. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_main.py +0 -0
  110. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_search_commands.py +0 -0
  111. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_space_commands.py +0 -0
  112. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_user_commands.py +0 -0
  113. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_cli/test_workspace_commands.py +0 -0
  114. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_config/__init__.py +0 -0
  115. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_config/test_settings.py +0 -0
  116. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_config/test_store.py +0 -0
  117. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_convert/__init__.py +0 -0
  118. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_convert/test_prosemirror_to_md.py +0 -0
  119. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_docs/__init__.py +0 -0
  120. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_output/__init__.py +0 -0
  121. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_output/test_tree.py +0 -0
  122. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_sync/__init__.py +0 -0
  123. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_sync/test_frontmatter.py +0 -0
  124. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_sync/test_manifest.py +0 -0
  125. {docmost_cli-0.6.0 → docmost_cli-0.7.0}/tests/test_sync/test_pull.py +0 -0
@@ -8,6 +8,9 @@ name: CI
8
8
  # Pins are the current majors, which target Node 24. Node 20 is deprecated and
9
9
  # is being force-migrated on the runners, so checkout@v4 / setup-python@v5 would
10
10
  # have had a shelf life.
11
+ #
12
+ # actionlint is downloaded and checksummed rather than run as an action, for the
13
+ # same reason: one less third-party resolution in the path.
11
14
 
12
15
  on:
13
16
  push:
@@ -88,6 +91,34 @@ jobs:
88
91
  - name: Mypy (strict)
89
92
  run: python -m mypy src/
90
93
 
94
+ workflows:
95
+ name: lint workflows
96
+ runs-on: ubuntu-latest
97
+ # Bump both together: the checksum is for the linux_amd64 tarball of this
98
+ # exact version, published on the actionlint releases page.
99
+ env:
100
+ ACTIONLINT_VERSION: 1.7.12
101
+ ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
102
+ steps:
103
+ - uses: actions/checkout@v7
104
+
105
+ - name: Install actionlint
106
+ run: |
107
+ set -euo pipefail
108
+ curl -fsSL -o actionlint.tar.gz \
109
+ "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
110
+ echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum --check --strict
111
+ tar xzf actionlint.tar.gz actionlint
112
+
113
+ - name: Lint the workflows
114
+ # actionlint only shell-lints `run:` blocks when shellcheck is on PATH,
115
+ # and quietly skips them when it is not. Assert it, rather than trust the
116
+ # runner image to keep shipping it and lose half the coverage in silence.
117
+ run: |
118
+ set -euo pipefail
119
+ command -v shellcheck
120
+ ./actionlint -color
121
+
91
122
  build:
92
123
  name: build
93
124
  runs-on: ubuntu-latest
@@ -118,9 +118,12 @@ jobs:
118
118
  print(f"{wheel.name}: {len(pages)} man pages, licence metadata OK")
119
119
  PY
120
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.
121
+ # upload@v7 and download@v8 are a matched pair and should move together:
122
+ # v7 added the option to upload files as-is, and v8 is the major that
123
+ # stopped assuming every download is a zip.
124
+ #
125
+ # Keep `archive` at its default of true. Uploading as-is takes only a
126
+ # single file, and this step ships two — the wheel and the sdist.
124
127
  - uses: actions/upload-artifact@v7
125
128
  with:
126
129
  name: dist
@@ -209,7 +212,11 @@ jobs:
209
212
 
210
213
  # Only fill in notes if whoever created it left them empty —
211
214
  # never overwrite notes a human wrote by hand.
212
- if [ -z "$(gh release view "$tag" --json body --jq '.body' | tr -d '[:space:]')" ]; then
215
+ #
216
+ # `body` is a nullable string, and jq renders a null as the four
217
+ # characters "null" — non-empty, so without the `// ""` this test
218
+ # reads an empty release as one a human had already written.
219
+ if [ -z "$(gh release view "$tag" --json body --jq '.body // ""' | tr -d '[:space:]')" ]; then
213
220
  echo "Release body is empty; filling it from CHANGELOG.md."
214
221
  gh release edit "$tag" --notes-file release-notes.md
215
222
  else
@@ -1,5 +1,127 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 (2026-07-28)
4
+
5
+ ### Added
6
+
7
+ - **`attachment upload <page-id> --file <path>`** — upload a file and attach it
8
+ to a page, closing the last "can the CLI do what the editor does" gap for
9
+ images. Contributed by [@amanpatel](https://github.com/amanpatel) in
10
+ [#1](https://github.com/glinhard/docmost-cli/pull/1).
11
+
12
+ It posts a multipart upload to `POST /files/upload`, the undocumented endpoint
13
+ the web editor itself uses, and prints the attachment ID to stdout with the
14
+ page-embeddable URL on stderr — so `ID=$(docmost-cli attachment upload ...)`
15
+ captures the ID alone, and the file can be embedded with
16
+ `![alt](/api/files/<id>/<filename>)`. The MIME type is guessed from the
17
+ filename and falls back to `application/octet-stream`.
18
+
19
+ Verified end to end against Docmost Community: the file uploads, the returned
20
+ URL renders in the page, and the endpoint answers with the attachment row
21
+ bare, without the `{success, status, data}` envelope every other endpoint
22
+ uses. The response is read both ways regardless, since an undocumented
23
+ endpoint's shape is not a contract. A response carrying no usable record is a
24
+ clear error rather than a traceback, and a filename is percent-escaped so it
25
+ cannot reshape the URL path.
26
+
27
+ ### Fixed
28
+
29
+ - **`page import --title` never reached the server.** The flag was auto-detected,
30
+ printed in the confirmation message, and then dropped: the import endpoint has
31
+ no title parameter, so the page kept whatever title Docmost derived from the
32
+ file's first heading or its filename. The CLI said *"Imported 'X'"* about a
33
+ page called something else. This is the same defect as #12 one command over —
34
+ that fix landed inside `create_and_place_page`, which `page import` does not
35
+ go through. An explicit `--title` is now applied after the import; omitting it
36
+ still costs no extra call, since the server's own title is then the answer.
37
+ - **`sync status` and `sync push --dry-run` listed change types in an order that
38
+ varied between runs.** `PageChange.changes` is a set of enum members, and
39
+ `Enum.__hash__` hashes the member *name* — so iteration follows Python's
40
+ per-process string hash seed. The same working tree could print
41
+ `content_changed, title_changed` on one run and `title_changed,
42
+ content_changed` on the next, which matters most for `--dry-run`, whose stdout
43
+ is the scripting-facing action plan. Both now render through one helper that
44
+ sorts by declaration order.
45
+ - **`page export` could not recover from an expired session.** `post_raw` built
46
+ and sent its request directly instead of going through the shared retry path,
47
+ so the one command that uses it got no 401 re-authentication and no backoff on
48
+ 429/5xx, unlike every other call in the CLI. On Community edition, where auth
49
+ *is* a refreshable session, that made an expired token a hard failure. Silent
50
+ endpoint probes keep their single-shot behaviour, which the
51
+ Enterprise/Community content fallbacks depend on.
52
+ - `config test` no longer prints `Error: Configuration error: 1` underneath the
53
+ real error. It caught the `SystemExit` that `get_client()` raises after
54
+ reporting its own reason, and reformatted the exit *code* as the message.
55
+ - **`page create --title` was silently discarded when the Markdown began with
56
+ its own heading** ([#12](https://github.com/glinhard/docmost-cli/issues/12)).
57
+ The import endpoint derives the page title from the first H1, and the CLI
58
+ never applied the explicit title afterwards — so it reported creating
59
+ *"agt01 — Ferdl personal agent host"* while the page persisted as *"agt01"*.
60
+ The title is now always applied after import, folded into the same request as
61
+ `--icon` so it costs one call rather than two.
62
+
63
+ Note that Docmost's import *consumes* the content's first heading — **at any
64
+ level** — as the page title and removes it from the body. That is unchanged
65
+ by this fix and applies with or without `--title`, but it means a page
66
+ created from `# Heading` plus `--title "Other"` keeps that text neither in
67
+ the body nor as the title. Put it in `--title` if you need it.
68
+
69
+ Measured against Docmost Community, not inferred: content starting `##
70
+ Subheading` loses that line exactly as an `# H1` does, so dropping a level is
71
+ not a way to keep it.
72
+
73
+ This also fixes `sync push`: a frontmatter title edited without touching the
74
+ body's H1 used to be ignored on page creation.
75
+ - The man page's upload-and-embed example used `--content "![alt](...)"`, which
76
+ dies in an interactive `bash` with *event not found*: Markdown image syntax
77
+ starts with `!`, and history expansion applies inside double quotes. It now
78
+ pipes through `--stdin`, with the single-quote form as an alternative. Found
79
+ by running the documented command against a live instance.
80
+ - `docmost-cli.1` declares the `tbl` preprocessor, so its OUTPUT CONVENTIONS
81
+ table renders as a table rather than as raw tbl source, and the table now fits
82
+ an 80-column terminal instead of overflowing it. All ten pages are clean under
83
+ `groff -ww`.
84
+
85
+ ### Changed
86
+
87
+ - The release workflow no longer mistakes a null release body for hand-written
88
+ notes. GitHub types a release body as nullable and `jq` renders a null as the
89
+ text `null`, so the emptiness check read "no notes" as "notes a human wrote"
90
+ and skipped filling them from this file.
91
+ - `upload-artifact` and `download-artifact` moved to their Node 24 majors, which
92
+ the runners had begun force-migrating.
93
+ - CI lints the workflow files with actionlint, including shellcheck over every
94
+ `run:` block. Nothing checked them before, which is how an unresolvable action
95
+ pin, two deprecated runtimes and the null-body bug all reached `main` green.
96
+
97
+ ### Internal
98
+
99
+ No behaviour change in any of these; they are the structural half of the same
100
+ review that found the fixes above.
101
+
102
+ - `SyncDiff.move_only` replaces three hand-written copies of "moved pages that
103
+ are not already being updated". Two of them compared whole `PageChange`
104
+ objects — every field, including the Markdown body — while the third compared
105
+ page IDs, so the plan `sync push` *printed* and the plan it *executed* were
106
+ two different computations that happened to agree.
107
+ - `require_manifest()` in `sync/manifest.py` replaces three copies of
108
+ load-then-error-if-missing, and with them the two `return # unreachable`
109
+ lines that followed a `NoReturn` call.
110
+ - `output/formatter.py` now owns exactly one stdout and one stderr console, and
111
+ exposes `print_progress()` and `print_rendered()`. There were five consoles
112
+ before — two of them reached through `from ...formatter import _err_console`,
113
+ a private name, and two more rebuilt on every table render.
114
+ - `sync/push.py` and `sync/pull.py` import at module level. Twenty-two imports
115
+ sat inside function bodies without a cycle to justify them; `sync/__init__.py`
116
+ imports the whole package eagerly regardless. `api/pages.py` likewise deferred
117
+ imports from a module it already imports on line 7.
118
+ - `DocmostClient` gained a public `base_url` property, so `config test` stops
119
+ reading `client._base_url`, and a `_rebuild()` helper that replaces the
120
+ duplicated build-and-reauthenticate block in the retry loop.
121
+ - `page get` fetches the page once instead of once per branch, and the nine
122
+ subcommand registrations in `cli/main.py` are one import block instead of nine
123
+ `# noqa: E402` pairs.
124
+
3
125
  ## 0.6.0 (2026-07-27)
4
126
 
5
127
  ### Behavior changes
@@ -42,7 +42,10 @@ ruff format src/ tests/
42
42
  build derives from it) and add a `CHANGELOG.md` section.
43
43
  2. Update the `.TH` version line in all ten `man/man1/*.1` pages.
44
44
  `tests/test_docs/test_man_pages.py` fails if they drift.
45
- 3. Merge to `main`, then push a matching tag: `git tag v0.6.0 && git push origin v0.6.0`.
45
+ 3. Merge to `main`, then push a matching tag: `git tag v0.7.0 && git push origin v0.7.0`.
46
+ Without a local clone, use **Releases → Draft a new release → Choose a tag →
47
+ create it on publish**. The workflow handles a release that already exists,
48
+ and fills in notes from `CHANGELOG.md` only if the body was left empty.
46
49
 
47
50
  `.github/workflows/release.yml` takes it from there: full test suite, tag ↔
48
51
  `__version__` guard, a check that the version is not already on PyPI, build,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docmost-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: CLI tool for managing Docmost wiki instances
5
5
  Project-URL: Homepage, https://github.com/glinhard/docmost-cli
6
6
  Project-URL: Repository, https://github.com/glinhard/docmost-cli
@@ -116,7 +116,7 @@ docmost-cli page create <space-slug> --title "My Page" --file content.md
116
116
  | `docmost-cli page get <page-id>` | Get page content as Markdown (`--meta`, `--raw`) |
117
117
  | `docmost-cli page create <space-slug>` | Create a new page (`--title`, `--file`, `--stdin`) |
118
118
  | `docmost-cli page update <page-id>` | Update a page in place (`--title`, `--icon`, `--content`, `--file`, `--stdin`, `--append`, `--prepend`) |
119
- | `docmost-cli page delete <page-id>` | Delete a page (with confirmation, `--yes` to skip) |
119
+ | `docmost-cli page delete <page-id>` | Delete a page (with confirmation; the global `--yes` skips it, before the subcommand: `docmost-cli --yes page delete <id>`) |
120
120
  | `docmost-cli page move <page-id>` | Move a page (`--parent`, `--space`, `--root`, `--position first\|last\|<key>`) |
121
121
  | `docmost-cli page duplicate <page-id>` | Duplicate a page |
122
122
  | `docmost-cli page copy <page-id>` | Copy a page to another space (`--space`) |
@@ -132,6 +132,7 @@ docmost-cli page create <space-slug> --title "My Page" --file content.md
132
132
  | `docmost-cli comment update <comment-id>` | Edit a comment (`--content`) |
133
133
  | `docmost-cli search query <query>` | Full-text search (`--space`, `--type`, `--json`, pagination flags) |
134
134
  | `docmost-cli attachment search <query>` | Search attachments (`--space`, `--json`, pagination flags) |
135
+ | `docmost-cli attachment upload <page-id>` | Upload a file to a page (`--file`) |
135
136
  | `docmost-cli workspace info` | Show workspace details (`--json`, `--fields`) |
136
137
  | `docmost-cli workspace members` | List workspace members (`--json`) |
137
138
  | `docmost-cli user me` | Show authenticated user info (`--json`, `--fields`) |
@@ -77,7 +77,7 @@ docmost-cli page create <space-slug> --title "My Page" --file content.md
77
77
  | `docmost-cli page get <page-id>` | Get page content as Markdown (`--meta`, `--raw`) |
78
78
  | `docmost-cli page create <space-slug>` | Create a new page (`--title`, `--file`, `--stdin`) |
79
79
  | `docmost-cli page update <page-id>` | Update a page in place (`--title`, `--icon`, `--content`, `--file`, `--stdin`, `--append`, `--prepend`) |
80
- | `docmost-cli page delete <page-id>` | Delete a page (with confirmation, `--yes` to skip) |
80
+ | `docmost-cli page delete <page-id>` | Delete a page (with confirmation; the global `--yes` skips it, before the subcommand: `docmost-cli --yes page delete <id>`) |
81
81
  | `docmost-cli page move <page-id>` | Move a page (`--parent`, `--space`, `--root`, `--position first\|last\|<key>`) |
82
82
  | `docmost-cli page duplicate <page-id>` | Duplicate a page |
83
83
  | `docmost-cli page copy <page-id>` | Copy a page to another space (`--space`) |
@@ -93,6 +93,7 @@ docmost-cli page create <space-slug> --title "My Page" --file content.md
93
93
  | `docmost-cli comment update <comment-id>` | Edit a comment (`--content`) |
94
94
  | `docmost-cli search query <query>` | Full-text search (`--space`, `--type`, `--json`, pagination flags) |
95
95
  | `docmost-cli attachment search <query>` | Search attachments (`--space`, `--json`, pagination flags) |
96
+ | `docmost-cli attachment upload <page-id>` | Upload a file to a page (`--file`) |
96
97
  | `docmost-cli workspace info` | Show workspace details (`--json`, `--fields`) |
97
98
  | `docmost-cli workspace members` | List workspace members (`--json`) |
98
99
  | `docmost-cli user me` | Show authenticated user info (`--json`, `--fields`) |
@@ -167,7 +167,7 @@ docmost-cli page get <page-id> # Get page content as Markdown
167
167
  --meta # Prepend YAML frontmatter (id, title, space, dates)
168
168
 
169
169
  docmost-cli page create <space-slug> # Create a new page
170
- --title "Page Title" # Required: page title
170
+ --title "Page Title" # Required; wins over a leading heading
171
171
  --content "Markdown string" # Content as inline string
172
172
  --file path/to/content.md # Content from file
173
173
  --stdin # Content from stdin
@@ -269,6 +269,8 @@ docmost-cli search <query> # Full-text search
269
269
  ```
270
270
  docmost-cli attachment search <query> # Search attachments
271
271
  --space <space-slug>
272
+ docmost-cli attachment upload <page-id> # Upload a file to a page
273
+ --file <path>
272
274
  ```
273
275
 
274
276
  ### 4.8 `docmost-cli workspace`
@@ -480,6 +482,8 @@ POST /search → {query, spaceId?, type?, limit?, cursor?}
480
482
  ```
481
483
  POST /attachments/search → {query, spaceId?, limit?, cursor?}
482
484
  GET /attachments/... → file download
485
+ POST /files/upload → multipart {file, pageId} (undocumented; the endpoint
486
+ the web editor uses for inline images/attachments)
483
487
  ```
484
488
 
485
489
  **Workspace:**
@@ -805,7 +809,7 @@ def print_error(message: str, exit_code: int = 1) -> NoReturn:
805
809
  - [x] `docmost-cli page duplicate` / `page copy`
806
810
  - [x] `docmost-cli page children` (with `--json`) / `page history` (with `--json`)
807
811
  - [x] `docmost-cli page export` / `page import`
808
- - [x] `docmost-cli attachment search`
812
+ - [x] `docmost-cli attachment search` / `attachment upload`
809
813
  - [x] `docmost-cli workspace` / `docmost-cli user`
810
814
  - [x] Tree view (`--tree`) for page listing
811
815
  - [x] Pagination auto-follow for full listings
@@ -983,8 +987,25 @@ These items need investigation during implementation. Update this section as ans
983
987
  'page.<id>', …)` — **the server performs the Yjs write**. REST is
984
988
  sufficient; no WS client, no CRDT dependency.
985
989
  - [ ] **Rate limiting**: Does Docmost implement rate limiting? If so, what are the limits?
986
- - [ ] **Attachment upload**: Is there an API endpoint for uploading attachments, or
987
- is it only available through the editor UI?
990
+ - [x] **Attachment upload**: *Resolved*: `POST /files/upload` (multipart, fields
991
+ `file` + `pageId`) is the undocumented endpoint the web editor uses for
992
+ inline images and attachments. The page-embeddable URL is
993
+ `/api/files/{id}/{fileName}`. Wired up as
994
+ `docmost-cli attachment upload <page-id> --file <path>`.
995
+
996
+ Verified against Docmost Community on 2026-07-28: it returns the
997
+ attachment row **bare, without** the `{success, status, data}` envelope
998
+ every other endpoint uses. Observed keys:
999
+
1000
+ ```
1001
+ aiChatId, createdAt, creatorId, deletedAt, fileExt, fileName, filePath,
1002
+ fileSize, id, mimeType, pageId, spaceId, type, updatedAt, workspaceId
1003
+ ```
1004
+
1005
+ `build_attachment_url()` still routes through `unwrap_data()`, so it keeps
1006
+ working if the endpoint is ever brought under the standard interceptor.
1007
+ That costs nothing and the endpoint is undocumented, so its shape is not
1008
+ contractual.
988
1009
  - [x] **Space slug vs ID**: Some endpoints accept slug, others require ID.
989
1010
  *Resolved*: `resolve_space_id()` helper in `api/spaces.py` calls
990
1011
  `POST /spaces/info` with `{spaceSlug: slug}` and returns the ID.
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-attachment(1)
2
2
  .\" Source: SPECIFICATION.md section 4.6, src/docmost_cli/cli/attachment.py
3
- .TH DOCMOST\-CLI\-ATTACHMENT 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-ATTACHMENT 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-attachment \- attachment operations for Docmost wiki
6
6
  .SH SYNOPSIS
@@ -9,6 +9,11 @@ docmost\-cli\-attachment \- attachment operations for Docmost wiki
9
9
  .RB [ \-\-space
10
10
  .IR slug ]
11
11
  .RB [ \-\-json ]
12
+ .PP
13
+ .B docmost\-cli attachment upload
14
+ .I page\-id
15
+ .B \-\-file
16
+ .I path
12
17
  .SH DESCRIPTION
13
18
  Commands for working with file attachments in a Docmost wiki.
14
19
  .SH SUBCOMMANDS
@@ -59,6 +64,28 @@ and
59
64
  .PP
60
65
  Default table columns: id, fileName, type.
61
66
  These affect the table only; \-\-json emits every field the server returns.
67
+ .SS upload
68
+ Upload a file (e.g. an image) and attach it to a page.
69
+ .PP
70
+ .I page\-id
71
+ is the UUID of the page to attach the file to.
72
+ .TP
73
+ .BI \-\-file " path"
74
+ Path to the local file to upload.
75
+ The MIME type is guessed from the filename, falling back to
76
+ .BR application/octet\-stream .
77
+ .PP
78
+ Prints the new attachment ID to stdout and a confirmation carrying the
79
+ page\-embeddable URL (\fI/api/files/{id}/{fileName}\fR) to stderr, so
80
+ .B ID=$(docmost\-cli attachment upload ...)
81
+ captures the ID alone.
82
+ Embed the file in a page's Markdown with
83
+ .IR "![alt text](/api/files/<id>/<filename>)" .
84
+ .PP
85
+ This uses
86
+ .IR /files/upload ,
87
+ which is not part of Docmost's documented REST API; it is the endpoint the web
88
+ editor itself uses for inline images and attachments.
62
89
  .SH EXAMPLES
63
90
  Search for attachments:
64
91
  .PP
@@ -75,6 +102,45 @@ Search within a space:
75
102
  $ docmost\-cli attachment search "logo" \-\-space design
76
103
  .EE
77
104
  .RE
105
+ .PP
106
+ Upload an image to a page:
107
+ .PP
108
+ .RS 4
109
+ .EX
110
+ $ docmost\-cli attachment upload 019e997e\-e14c\-7de7\-a35c\-15089aaa306f \\
111
+ \-\-file ./diagram.png
112
+ .EE
113
+ .RE
114
+ .PP
115
+ Upload an image and embed it in the page in one go:
116
+ .PP
117
+ .RS 4
118
+ .EX
119
+ $ ID=$(docmost\-cli attachment upload "$PAGE" \-\-file ./diagram.png)
120
+ $ printf '![diagram](/api/files/%s/diagram.png)\\n' "$ID" \\
121
+ | docmost\-cli page update "$PAGE" \-\-append \-\-stdin
122
+ .EE
123
+ .RE
124
+ .PP
125
+ Markdown image syntax starts with
126
+ .BR ! ,
127
+ which an interactive
128
+ .BR bash (1)
129
+ treats as a history expansion inside double quotes, so
130
+ .B \-\-content "![...]"
131
+ fails with
132
+ .IR "event not found" .
133
+ Piping through
134
+ .B \-\-stdin
135
+ as above sidesteps the shell entirely; single quotes also work, since
136
+ history expansion does not apply inside them:
137
+ .PP
138
+ .RS 4
139
+ .EX
140
+ $ docmost\-cli page update "$PAGE" \-\-append \\
141
+ \-\-content '![diagram](/api/files/'"$ID"'/diagram.png)'
142
+ .EE
143
+ .RE
78
144
  .SH EXIT STATUS
79
145
  See
80
146
  .BR docmost\-cli (1).
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-comment(1)
2
2
  .\" Source: SPECIFICATION.md section 4.4, src/docmost_cli/cli/comment.py
3
- .TH DOCMOST\-CLI\-COMMENT 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-COMMENT 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-comment \- comment operations for Docmost wiki pages
6
6
  .SH SYNOPSIS
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-config(1)
2
2
  .\" Source: SPECIFICATION.md section 4.1, src/docmost_cli/cli/config_cmd.py
3
- .TH DOCMOST\-CLI\-CONFIG 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-CONFIG 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-config \- manage docmost\-cli configuration
6
6
  .SH SYNOPSIS
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-page(1)
2
2
  .\" Source: SPECIFICATION.md section 4.2, src/docmost_cli/cli/page.py
3
- .TH DOCMOST\-CLI\-PAGE 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-PAGE 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-page \- page operations for Docmost wiki
6
6
  .SH SYNOPSIS
@@ -190,6 +190,8 @@ identifies the space to create the page in.
190
190
  .BI \-\-title " title"
191
191
  Page title.
192
192
  Required.
193
+ This is the title the page is saved with, even when the Markdown starts with a
194
+ heading of its own.
193
195
  .TP
194
196
  .BI \-\-content " markdown"
195
197
  Inline Markdown content string.
@@ -220,6 +222,20 @@ When set, the new page is nested under this parent.
220
222
  Page icon emoji.
221
223
  .PP
222
224
  Outputs the new page ID to stdout and a confirmation message to stderr.
225
+ .PP
226
+ Docmost's import consumes the content's first heading \(em at any level \(em as
227
+ the page title, and removes it from the body. That happens with or without
228
+ .BR \-\-title ,
229
+ so content beginning with
230
+ .I "# Heading"
231
+ or
232
+ .I "## Heading"
233
+ loses that line either way; with
234
+ .B \-\-title
235
+ the page is at least titled as asked, rather than after the discarded heading.
236
+ Put the text in
237
+ .B \-\-title
238
+ if you need it. Starting a level lower does not preserve it.
223
239
  .SS update
224
240
  Update an existing page's title, icon, and/or content.
225
241
  .PP
@@ -418,6 +434,8 @@ Required.
418
434
  .TP
419
435
  .BI \-\-title " title"
420
436
  Override the page title.
437
+ This is the title the page is saved with, even when the file starts with a
438
+ heading of its own.
421
439
  If omitted, the title is auto\-detected from the filename or
422
440
  the first H1 heading in the file.
423
441
  .TP
@@ -425,6 +443,12 @@ the first H1 heading in the file.
425
443
  Parent page UUID to nest the imported page under.
426
444
  .PP
427
445
  Outputs the new page ID to stdout.
446
+ .PP
447
+ As with
448
+ .BR "page create" ,
449
+ Docmost's import consumes a leading heading: it becomes the page title and is
450
+ removed from the body, with or without
451
+ .BR \-\-title .
428
452
  .SH EXAMPLES
429
453
  List all pages in a space:
430
454
  .PP
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-search(1)
2
2
  .\" Source: SPECIFICATION.md section 4.5, src/docmost_cli/cli/search.py
3
- .TH DOCMOST\-CLI\-SEARCH 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-SEARCH 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-search \- full\-text search across a Docmost wiki
6
6
  .SH SYNOPSIS
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-space(1)
2
2
  .\" Source: SPECIFICATION.md section 4.3, src/docmost_cli/cli/space.py
3
- .TH DOCMOST\-CLI\-SPACE 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-SPACE 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-space \- manage Docmost wiki spaces
6
6
  .SH SYNOPSIS
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-sync(1)
2
2
  .\" Source: SPECIFICATION.md section 4.10, src/docmost_cli/cli/sync_cmd.py
3
- .TH DOCMOST\-CLI\-SYNC 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-SYNC 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-sync \- synchronize Docmost pages with local Markdown files
6
6
  .SH SYNOPSIS
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-user(1)
2
2
  .\" Source: SPECIFICATION.md section 4.8, src/docmost_cli/cli/user.py
3
- .TH DOCMOST\-CLI\-USER 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-USER 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-user \- current user information for Docmost
6
6
  .SH SYNOPSIS
@@ -1,6 +1,6 @@
1
1
  .\" Manual page for docmost-cli-workspace(1)
2
2
  .\" Source: SPECIFICATION.md section 4.7, src/docmost_cli/cli/workspace.py
3
- .TH DOCMOST\-CLI\-WORKSPACE 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
3
+ .TH DOCMOST\-CLI\-WORKSPACE 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
4
  .SH NAME
5
5
  docmost\-cli\-workspace \- Docmost workspace information
6
6
  .SH SYNOPSIS
@@ -1,6 +1,7 @@
1
+ '\" t
1
2
  .\" Manual page for docmost-cli(1)
2
3
  .\" Source: SPECIFICATION.md, src/docmost_cli/cli/main.py
3
- .TH DOCMOST\-CLI 1 "2026\-03\-22" "docmost\-cli 0.6.0" "User Commands"
4
+ .TH DOCMOST\-CLI 1 "2026\-03\-22" "docmost\-cli 0.7.0" "User Commands"
4
5
  .SH NAME
5
6
  docmost\-cli \- manage Docmost wiki instances from the terminal
6
7
  .SH SYNOPSIS
@@ -146,10 +147,10 @@ l l l.
146
147
  Command type stdout stderr
147
148
  _
148
149
  Content (page get) Raw Markdown nothing
149
- Content + meta (page get \-\-meta) YAML frontmatter + Markdown nothing
150
- Lists (page list, search, ...) Rich table (curated columns) or complete JSON (\-\-json) nothing
151
- Single item (user me, workspace info) Key\-value table or JSON (\-\-json) nothing
152
- Writes (page create, delete, ...) Resource ID only Confirmation message
150
+ Content + meta Frontmatter + Markdown nothing
151
+ Lists (page list) Table or JSON (\-\-json) nothing
152
+ Single item (user me) Key\-value or JSON nothing
153
+ Writes (page create) Resource ID only Confirmation message
153
154
  Sync status Change summary nothing
154
155
  Sync push \-\-dry\-run Action plan nothing
155
156
  Sync ops (pull, push) nothing Progress + summary
@@ -1,5 +1,5 @@
1
1
  """docmost-cli: CLI tool for managing Docmost wiki instances."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.0"
4
4
 
5
5
  __all__ = ["__version__"]