mdview-cli 0.1.0__tar.gz → 1.0.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 (26) hide show
  1. mdview_cli-1.0.0/.github/workflows/release.yml +53 -0
  2. mdview_cli-1.0.0/.gitignore +6 -0
  3. mdview_cli-1.0.0/PKG-INFO +160 -0
  4. mdview_cli-1.0.0/README.md +142 -0
  5. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/pyproject.toml +3 -3
  6. mdview_cli-1.0.0/src/mdview_cli/__init__.py +0 -0
  7. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/api.py +3 -0
  8. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/cli.py +98 -23
  9. mdview_cli-1.0.0/src/mdview_cli.egg-info/PKG-INFO +160 -0
  10. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/SOURCES.txt +6 -1
  11. mdview_cli-1.0.0/src/mdview_cli.egg-info/scm_file_list.json +16 -0
  12. mdview_cli-1.0.0/src/mdview_cli.egg-info/scm_version.json +8 -0
  13. mdview_cli-1.0.0/tests/e2e/test_free_tools.py +75 -0
  14. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/tests/test_cli.py +101 -3
  15. mdview_cli-0.1.0/PKG-INFO +0 -63
  16. mdview_cli-0.1.0/README.md +0 -45
  17. mdview_cli-0.1.0/src/mdview_cli/__init__.py +0 -1
  18. mdview_cli-0.1.0/src/mdview_cli.egg-info/PKG-INFO +0 -63
  19. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/LICENSE +0 -0
  20. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/setup.cfg +0 -0
  21. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/config.py +0 -0
  22. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli/state.py +0 -0
  23. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/dependency_links.txt +0 -0
  24. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/entry_points.txt +0 -0
  25. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/requires.txt +0 -0
  26. {mdview_cli-0.1.0 → mdview_cli-1.0.0}/src/mdview_cli.egg-info/top_level.txt +0 -0
@@ -0,0 +1,53 @@
1
+ name: CLI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ branches:
8
+ - main
9
+ pull_request:
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: actions/setup-python@v6
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+ - run: python -m pip install -e '.[test]'
23
+ - run: python -m pytest tests/test_cli.py
24
+
25
+ e2e-free-tools:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ - uses: actions/setup-python@v6
30
+ with:
31
+ python-version: "3.12"
32
+ - run: python -m pip install -e '.[test]'
33
+ - run: python -m pytest tests/e2e
34
+
35
+ publish:
36
+ if: startsWith(github.ref, 'refs/tags/v')
37
+ needs: [test, e2e-free-tools]
38
+ runs-on: ubuntu-latest
39
+ environment: pypi
40
+ permissions:
41
+ id-token: write
42
+ steps:
43
+ - uses: actions/checkout@v7
44
+ with:
45
+ fetch-depth: 0
46
+ - uses: actions/setup-python@v6
47
+ with:
48
+ python-version: "3.12"
49
+ - run: python -m pip install build
50
+ - run: python -m build
51
+ - uses: pypa/gh-action-pypi-publish@release/v1
52
+ with:
53
+ packages-dir: dist/
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pytest_cache/
4
+ build/
5
+ dist/
6
+ .venv/
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdview-cli
3
+ Version: 1.0.0
4
+ Summary: Iterate on Markdown and verify every revision with mdview.io
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://mdview.io
7
+ Project-URL: Repository, https://github.com/gogainda/mdview-cli
8
+ Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: click>=8.1
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: platformdirs>=4.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=8.0; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # mdv — preview Markdown files from your terminal
20
+
21
+ `mdv` renders a Markdown file the way it should look — headings, tables, code,
22
+ Mermaid diagrams — and opens it in your browser. One command, no account, no
23
+ configuration.
24
+
25
+ ```bash
26
+ uv tool install mdview-cli # or: pipx install mdview-cli, pip install mdview-cli
27
+ mdv notes.md
28
+ ```
29
+
30
+ Homebrew users:
31
+
32
+ ```bash
33
+ brew install gogainda/tap/mdview-cli
34
+ ```
35
+
36
+ That's the whole workflow: `mdv FILE.md` publishes the file to
37
+ [mdview.io](https://mdview.io), opens the rendered page, and prints a link you
38
+ can send to anyone. Anonymous previews last 30 days and take files up to 2 MB.
39
+
40
+ ## Two ways to preview
41
+
42
+ **Shareable preview (default).** `mdv FILE.md` is shorthand for `mdv preview
43
+ FILE.md`. The title comes from the first `#` heading, or pass your own:
44
+
45
+ ```bash
46
+ mdv notes.md
47
+ mdv preview notes.md --title "Design notes"
48
+ mdv preview notes.md --json # print the share URLs as JSON, don't open a browser
49
+ ```
50
+
51
+ **Local-only preview.** `mdv open` uploads nothing: it packs the file into the
52
+ URL itself and rendering happens entirely in your browser. Use it for drafts
53
+ that shouldn't leave your machine.
54
+
55
+ ```bash
56
+ mdv open notes.md
57
+ ```
58
+
59
+ ## Iterating on one document
60
+
61
+ Previews are throwaway: every run mints a new link. When you keep working on
62
+ the same file — a spec, an architecture doc, a report — you want the opposite:
63
+ one stable URL whose content follows your edits, and a check that every
64
+ revision still renders cleanly.
65
+
66
+ Set up a free mdview.io CLI token once, then it's one command per iteration:
67
+
68
+ ```bash
69
+ mdv keys set # once (or set MDVIEW_TOKEN in CI)
70
+ mdv sync spec.md # publish this revision, verify it, auto-repair broken diagrams
71
+ ```
72
+
73
+ While you iterate, the document lives at its **private** `/p/` page — only
74
+ you, signed in, can see it, and the URL stays the same forever. Every sync
75
+ shows the fixed, renderable version there. Broken Mermaid diagrams are
76
+ repaired automatically and the fix is written back into your file (the
77
+ original is kept as a timestamped backup); only documents that can't be
78
+ repaired exit `1`.
79
+
80
+ When — and only when — you decide to publish, make it public; and grab a PDF
81
+ at any point:
82
+
83
+ ```bash
84
+ mdv share spec.md # mint the public /s/ link (from then on it follows your syncs)
85
+ mdv export spec.md # render the current revision to spec.pdf
86
+ ```
87
+
88
+ Useful around the loop:
89
+
90
+ ```bash
91
+ mdv sync spec.md --no-fix # fast sync: skip auto-repair, just report failures
92
+ mdv export spec.md --no-sync # PDF of the last synced revision; local edits stay private
93
+ mdv list # all saved documents with their URLs
94
+ mdv unlink spec.md # forget the file↔document link; the document stays online
95
+ ```
96
+
97
+ Nothing is public until you run `mdv share`. After that, the public page
98
+ serves the last synced revision — mid-edit local changes still stay private
99
+ until you sync. `mdv fix FILE --local` repairs a file in place without
100
+ creating a saved document (free accounts share the daily Quick Fix limit;
101
+ Pro is unlimited).
102
+
103
+ ### Example session
104
+
105
+ A real session, working on a doc whose Mermaid diagrams start out broken:
106
+
107
+ ```console
108
+ $ mdv sync spec.md # iterate — the broken flowchart is auto-repaired (~30s)
109
+ Document: 8be0be9198400443
110
+ Private: https://mdview.io/p/8be0be9198400443
111
+ Renderable: yes (1 diagrams, 0 tables)
112
+
113
+ $ vim spec.md # …add two sections, each with a (broken) diagram…
114
+ $ mdv sync spec.md # same doc, all three diagrams repaired and verified
115
+ Document: 8be0be9198400443
116
+ Private: https://mdview.io/p/8be0be9198400443
117
+ Renderable: yes (3 diagrams, 0 tables)
118
+
119
+ $ mdv share spec.md # happy with it — make it public
120
+ https://mdview.io/s/9a2b8fb1
121
+
122
+ $ mdv export spec.md # …and hand over a PDF
123
+ PDF: spec.pdf
124
+ ```
125
+
126
+ Two commands did all the work: `mdv sync` after every editing round, and
127
+ `mdv share` once at the end. The fixes were written back into `spec.md`
128
+ (originals in timestamped backups, so you can review what the repair
129
+ changed — worth a glance before sharing, since a repair is guaranteed to
130
+ render but not to read your mind), the private URL never changed, and the
131
+ public link didn't exist until it was asked for.
132
+
133
+ **When this flow fits:** a document that lives longer than one paste — specs,
134
+ architecture docs, reports, runbooks, agent-written docs you refine over
135
+ hours or days. Use it when reviewers follow one link that must always show
136
+ the latest good revision, when a PDF has to be producible at any moment, or
137
+ in CI as a docs gate (`mdv sync --no-fix` exits `1` on broken diagrams
138
+ without editing anything). For a one-off snapshot, `mdv preview` is enough;
139
+ for reading a local file without uploading it, use `mdv open`.
140
+
141
+ ## Reference
142
+
143
+ | Command | Token | What it does |
144
+ |---|---|---|
145
+ | `mdv FILE.md` / `mdv preview` | no | Publish anonymously, open the rendered page (30-day link) |
146
+ | `mdv open` | no | Render locally in the browser; nothing is uploaded |
147
+ | `mdv sync` | yes | Update a saved document at its private /p/ URL, verify + auto-repair |
148
+ | `mdv share` | yes | Make a synced document public at its /s/ URL |
149
+ | `mdv verify` | yes | Re-check rendering of a synced document |
150
+ | `mdv fix` | yes | Repair broken Mermaid diagrams, with a local backup |
151
+ | `mdv export` | yes | Export a synced document to PDF |
152
+ | `mdv list` | yes | List saved documents |
153
+ | `mdv unlink` | no | Remove the local file↔document association |
154
+ | `mdv keys set/list/path/unset` | — | Manage the stored CLI token |
155
+
156
+ Exit codes: `0` success, `1` document doesn't render cleanly, `2` usage error,
157
+ `3` mdview.io error.
158
+
159
+ Environment: `MDVIEW_TOKEN` supplies the token without storing it (ideal for
160
+ CI); `MDVIEW_BASE_URL` points the CLI at another server.
@@ -0,0 +1,142 @@
1
+ # mdv — preview Markdown files from your terminal
2
+
3
+ `mdv` renders a Markdown file the way it should look — headings, tables, code,
4
+ Mermaid diagrams — and opens it in your browser. One command, no account, no
5
+ configuration.
6
+
7
+ ```bash
8
+ uv tool install mdview-cli # or: pipx install mdview-cli, pip install mdview-cli
9
+ mdv notes.md
10
+ ```
11
+
12
+ Homebrew users:
13
+
14
+ ```bash
15
+ brew install gogainda/tap/mdview-cli
16
+ ```
17
+
18
+ That's the whole workflow: `mdv FILE.md` publishes the file to
19
+ [mdview.io](https://mdview.io), opens the rendered page, and prints a link you
20
+ can send to anyone. Anonymous previews last 30 days and take files up to 2 MB.
21
+
22
+ ## Two ways to preview
23
+
24
+ **Shareable preview (default).** `mdv FILE.md` is shorthand for `mdv preview
25
+ FILE.md`. The title comes from the first `#` heading, or pass your own:
26
+
27
+ ```bash
28
+ mdv notes.md
29
+ mdv preview notes.md --title "Design notes"
30
+ mdv preview notes.md --json # print the share URLs as JSON, don't open a browser
31
+ ```
32
+
33
+ **Local-only preview.** `mdv open` uploads nothing: it packs the file into the
34
+ URL itself and rendering happens entirely in your browser. Use it for drafts
35
+ that shouldn't leave your machine.
36
+
37
+ ```bash
38
+ mdv open notes.md
39
+ ```
40
+
41
+ ## Iterating on one document
42
+
43
+ Previews are throwaway: every run mints a new link. When you keep working on
44
+ the same file — a spec, an architecture doc, a report — you want the opposite:
45
+ one stable URL whose content follows your edits, and a check that every
46
+ revision still renders cleanly.
47
+
48
+ Set up a free mdview.io CLI token once, then it's one command per iteration:
49
+
50
+ ```bash
51
+ mdv keys set # once (or set MDVIEW_TOKEN in CI)
52
+ mdv sync spec.md # publish this revision, verify it, auto-repair broken diagrams
53
+ ```
54
+
55
+ While you iterate, the document lives at its **private** `/p/` page — only
56
+ you, signed in, can see it, and the URL stays the same forever. Every sync
57
+ shows the fixed, renderable version there. Broken Mermaid diagrams are
58
+ repaired automatically and the fix is written back into your file (the
59
+ original is kept as a timestamped backup); only documents that can't be
60
+ repaired exit `1`.
61
+
62
+ When — and only when — you decide to publish, make it public; and grab a PDF
63
+ at any point:
64
+
65
+ ```bash
66
+ mdv share spec.md # mint the public /s/ link (from then on it follows your syncs)
67
+ mdv export spec.md # render the current revision to spec.pdf
68
+ ```
69
+
70
+ Useful around the loop:
71
+
72
+ ```bash
73
+ mdv sync spec.md --no-fix # fast sync: skip auto-repair, just report failures
74
+ mdv export spec.md --no-sync # PDF of the last synced revision; local edits stay private
75
+ mdv list # all saved documents with their URLs
76
+ mdv unlink spec.md # forget the file↔document link; the document stays online
77
+ ```
78
+
79
+ Nothing is public until you run `mdv share`. After that, the public page
80
+ serves the last synced revision — mid-edit local changes still stay private
81
+ until you sync. `mdv fix FILE --local` repairs a file in place without
82
+ creating a saved document (free accounts share the daily Quick Fix limit;
83
+ Pro is unlimited).
84
+
85
+ ### Example session
86
+
87
+ A real session, working on a doc whose Mermaid diagrams start out broken:
88
+
89
+ ```console
90
+ $ mdv sync spec.md # iterate — the broken flowchart is auto-repaired (~30s)
91
+ Document: 8be0be9198400443
92
+ Private: https://mdview.io/p/8be0be9198400443
93
+ Renderable: yes (1 diagrams, 0 tables)
94
+
95
+ $ vim spec.md # …add two sections, each with a (broken) diagram…
96
+ $ mdv sync spec.md # same doc, all three diagrams repaired and verified
97
+ Document: 8be0be9198400443
98
+ Private: https://mdview.io/p/8be0be9198400443
99
+ Renderable: yes (3 diagrams, 0 tables)
100
+
101
+ $ mdv share spec.md # happy with it — make it public
102
+ https://mdview.io/s/9a2b8fb1
103
+
104
+ $ mdv export spec.md # …and hand over a PDF
105
+ PDF: spec.pdf
106
+ ```
107
+
108
+ Two commands did all the work: `mdv sync` after every editing round, and
109
+ `mdv share` once at the end. The fixes were written back into `spec.md`
110
+ (originals in timestamped backups, so you can review what the repair
111
+ changed — worth a glance before sharing, since a repair is guaranteed to
112
+ render but not to read your mind), the private URL never changed, and the
113
+ public link didn't exist until it was asked for.
114
+
115
+ **When this flow fits:** a document that lives longer than one paste — specs,
116
+ architecture docs, reports, runbooks, agent-written docs you refine over
117
+ hours or days. Use it when reviewers follow one link that must always show
118
+ the latest good revision, when a PDF has to be producible at any moment, or
119
+ in CI as a docs gate (`mdv sync --no-fix` exits `1` on broken diagrams
120
+ without editing anything). For a one-off snapshot, `mdv preview` is enough;
121
+ for reading a local file without uploading it, use `mdv open`.
122
+
123
+ ## Reference
124
+
125
+ | Command | Token | What it does |
126
+ |---|---|---|
127
+ | `mdv FILE.md` / `mdv preview` | no | Publish anonymously, open the rendered page (30-day link) |
128
+ | `mdv open` | no | Render locally in the browser; nothing is uploaded |
129
+ | `mdv sync` | yes | Update a saved document at its private /p/ URL, verify + auto-repair |
130
+ | `mdv share` | yes | Make a synced document public at its /s/ URL |
131
+ | `mdv verify` | yes | Re-check rendering of a synced document |
132
+ | `mdv fix` | yes | Repair broken Mermaid diagrams, with a local backup |
133
+ | `mdv export` | yes | Export a synced document to PDF |
134
+ | `mdv list` | yes | List saved documents |
135
+ | `mdv unlink` | no | Remove the local file↔document association |
136
+ | `mdv keys set/list/path/unset` | — | Manage the stored CLI token |
137
+
138
+ Exit codes: `0` success, `1` document doesn't render cleanly, `2` usage error,
139
+ `3` mdview.io error.
140
+
141
+ Environment: `MDVIEW_TOKEN` supplies the token without storing it (ideal for
142
+ CI); `MDVIEW_BASE_URL` points the CLI at another server.
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=69"]
2
+ requires = ["setuptools>=69", "setuptools-scm>=8"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -29,8 +29,8 @@ mdv = "mdview_cli.cli:cli"
29
29
  [tool.setuptools.packages.find]
30
30
  where = ["src"]
31
31
 
32
- [tool.setuptools.dynamic]
33
- version = {attr = "mdview_cli.__version__"}
32
+ [tool.setuptools_scm]
33
+ fallback_version = "0.0.0"
34
34
 
35
35
  [tool.pytest.ini_options]
36
36
  testpaths = ["tests"]
File without changes
@@ -51,6 +51,9 @@ class MdviewApi:
51
51
  def fix(self, document_id):
52
52
  return self.request("POST", f"/api/documents/{document_id}/fix/diagrams", json={}).json()
53
53
 
54
+ def fix_markdown(self, content):
55
+ return self.request("POST", "/api/documents/fix/diagrams", json={"content": content}).json()
56
+
54
57
  def export_pdf(self, document_id):
55
58
  return self.request("GET", f"/api/documents/{document_id}/export/pdf").content
56
59
 
@@ -77,6 +77,7 @@ def result_payload(document_id, share, report, created=False):
77
77
  return {
78
78
  "document_id": document_id,
79
79
  "created": created,
80
+ "url": f"{base_url()}/p/{document_id}",
80
81
  "share_url": share_url(share) if share else None,
81
82
  "renderable": verdict(report),
82
83
  "diagrams": report.get("diagrams", {}),
@@ -90,6 +91,8 @@ def print_result(payload, as_json=False, *, check=True):
90
91
  click.echo(json.dumps(payload, separators=(",", ":")))
91
92
  else:
92
93
  click.echo(f"Document: {payload['document_id']}")
94
+ if payload.get("url"):
95
+ click.echo(f"Private: {payload['url']}")
93
96
  if payload.get("share_url"):
94
97
  click.echo(f"Share: {payload['share_url']}")
95
98
  diagrams = payload.get("diagrams") or {}
@@ -109,7 +112,7 @@ def print_result(payload, as_json=False, *, check=True):
109
112
  raise click.exceptions.Exit(1)
110
113
 
111
114
 
112
- def sync_file(path: Path, *, api=None, state=None, title=None, document_id=None, share=True, verify=True):
115
+ def sync_file(path: Path, *, api=None, state=None, title=None, document_id=None, share=False, verify=True):
113
116
  if not path.is_file():
114
117
  raise click.UsageError(f"File not found: {path}")
115
118
  markdown = path.read_text(encoding="utf-8")
@@ -198,19 +201,56 @@ def keys_list():
198
201
  click.echo("default" if get_token() else "No token configured.")
199
202
 
200
203
 
204
+ def repair_synced_file(file, api, state, document_id):
205
+ repaired = api.fix(document_id)
206
+ markdown = repaired.get("markdown")
207
+ if markdown is None:
208
+ raise ServiceError("The repair response did not include Markdown.")
209
+ backup = backup_file(file, document_id)
210
+ atomic_write(file, markdown)
211
+ payload = sync_file(file, api=api, state=state)
212
+ payload["backup"] = str(backup)
213
+ payload["fixed"] = repaired.get("diagrams", {}).get("fixed", 0)
214
+ return payload
215
+
216
+
201
217
  @cli.command("sync")
202
218
  @click.argument("file", type=click.Path(path_type=Path))
219
+ @click.option("--no-fix", "no_fix", is_flag=True, help="Skip auto-repair; just report failing diagrams.")
203
220
  @click.option("--title")
204
221
  @click.option("--id", "document_id")
205
- @click.option("--no-share", is_flag=True)
206
222
  @click.option("--json", "as_json", is_flag=True)
207
223
  @service_errors
208
- def sync_command(file, title, document_id, no_share, as_json):
209
- """Update FILE, preserve its URL, and verify PDF rendering."""
210
- payload = sync_file(file, title=title, document_id=document_id, share=not no_share)
224
+ def sync_command(file, no_fix, title, document_id, as_json):
225
+ """Sync FILE to its private /p/ page, repairing broken diagrams on the way."""
226
+ api = api_for_token()
227
+ state = DocumentState()
228
+ payload = sync_file(file, api=api, state=state, title=title, document_id=document_id)
229
+ if not payload["renderable"] and not no_fix:
230
+ try:
231
+ payload = repair_synced_file(file, api, state, payload["document_id"])
232
+ except ApiError as error:
233
+ click.secho(f"Auto-repair unavailable: {error}", fg="yellow", err=True)
211
234
  print_result(payload, as_json)
212
235
 
213
236
 
237
+ @cli.command("share")
238
+ @click.argument("file", required=False, type=click.Path(path_type=Path))
239
+ @click.option("--id", "document_id")
240
+ @service_errors
241
+ def share_command(file, document_id):
242
+ """Make FILE's document public and print its share URL."""
243
+ state = DocumentState()
244
+ association = state.get(base_url(), file) if file else None
245
+ document_id = document_id or (association and association["document_id"])
246
+ if not document_id:
247
+ raise click.UsageError("Provide a synced FILE or --id.")
248
+ sid = share_id(api_for_token().share(document_id))
249
+ if file:
250
+ state.put(base_url(), file, document_id, sid, None)
251
+ click.echo(share_url(sid))
252
+
253
+
214
254
  @cli.command("verify")
215
255
  @click.argument("file", required=False, type=click.Path(path_type=Path))
216
256
  @click.option("--id", "document_id")
@@ -227,42 +267,77 @@ def verify_command(file, document_id, status, as_json):
227
267
  print_result(payload, as_json)
228
268
 
229
269
 
270
+ def backup_file(path: Path, key: str) -> Path:
271
+ backup_dir = data_dir() / "backups" / key
272
+ backup_dir.mkdir(parents=True, exist_ok=True)
273
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
274
+ backup = backup_dir / f"{stamp}-{path.name}"
275
+ shutil.copy2(path, backup)
276
+ return backup
277
+
278
+
230
279
  @cli.command("fix")
231
280
  @click.argument("file", type=click.Path(path_type=Path))
281
+ @click.option("--local", "local_only", is_flag=True, help="Repair the file in place without creating a saved document.")
232
282
  @click.option("--json", "as_json", is_flag=True)
233
283
  @service_errors
234
- def fix_command(file, as_json):
284
+ def fix_command(file, local_only, as_json):
285
+ if local_only:
286
+ if not file.is_file():
287
+ raise click.UsageError(f"File not found: {file}")
288
+ report = api_for_token().fix_markdown(file.read_text(encoding="utf-8"))
289
+ diagrams = report.get("diagrams", {})
290
+ payload = {
291
+ "renderable": bool(report.get("renderable")),
292
+ "fixed": diagrams.get("fixed", 0),
293
+ "remaining": diagrams.get("remaining", 0),
294
+ "backup": None,
295
+ }
296
+ markdown = report.get("markdown")
297
+ if payload["fixed"] and markdown:
298
+ payload["backup"] = str(backup_file(file, "local"))
299
+ atomic_write(file, markdown)
300
+ if as_json:
301
+ click.echo(json.dumps(payload, separators=(",", ":")))
302
+ else:
303
+ if payload["backup"]:
304
+ click.echo(f"Fixed {payload['fixed']} diagram(s); backup: {payload['backup']}")
305
+ if payload["renderable"]:
306
+ click.secho("Renderable: yes", fg="green")
307
+ else:
308
+ click.secho(f"Renderable: no ({payload['remaining']} diagrams still failing)", fg="red", err=True)
309
+ if not payload["renderable"]:
310
+ raise click.exceptions.Exit(1)
311
+ return
235
312
  api = api_for_token()
236
313
  state = DocumentState()
237
314
  initial = sync_file(file, api=api, state=state, verify=False)
238
- repaired = api.fix(initial["document_id"])
239
- markdown = repaired.get("markdown")
240
- if markdown is None:
241
- raise ServiceError("The repair response did not include Markdown.")
242
- backup_dir = data_dir() / "backups" / initial["document_id"]
243
- backup_dir.mkdir(parents=True, exist_ok=True)
244
- stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
245
- backup = backup_dir / f"{stamp}-{file.name}"
246
- shutil.copy2(file, backup)
247
- atomic_write(file, markdown)
248
- payload = sync_file(file, api=api, state=state)
249
- payload["backup"] = str(backup)
250
- payload["fixed"] = repaired.get("diagrams", {}).get("fixed", 0)
315
+ payload = repair_synced_file(file, api, state, initial["document_id"])
251
316
  print_result(payload, as_json)
252
317
 
253
318
 
254
319
  @cli.command("export")
255
- @click.argument("file", type=click.Path(path_type=Path))
320
+ @click.argument("file", required=False, type=click.Path(path_type=Path))
256
321
  @click.option("-o", "--output", type=click.Path(path_type=Path))
322
+ @click.option("--id", "document_id")
323
+ @click.option("--no-sync", "no_sync", is_flag=True, help="Export the published document as-is, without uploading local changes.")
257
324
  @click.option("--force", is_flag=True)
258
325
  @service_errors
259
- def export_command(file, output, force):
326
+ def export_command(file, output, document_id, no_sync, force):
260
327
  api = api_for_token()
261
- payload = sync_file(file, api=api)
328
+ if no_sync or file is None:
329
+ association = DocumentState().get(base_url(), file) if file else None
330
+ document_id = document_id or (association and association["document_id"])
331
+ if not document_id:
332
+ raise click.UsageError("Provide an associated FILE or --id.")
333
+ report = api.verify(document_id)
334
+ payload = result_payload(document_id, association and association["share_id"], report)
335
+ else:
336
+ payload = sync_file(file, api=api, document_id=document_id)
262
337
  print_result(payload, check=False)
263
338
  if not payload["renderable"] and not force:
264
339
  raise click.ClickException("Document is not Renderable; use --force to export anyway.")
265
- output = output or file.with_suffix(".pdf")
340
+ output = output or (file.with_suffix(".pdf") if file else Path(f"{payload['document_id']}.pdf"))
266
341
  pdf = api.export_pdf(payload["document_id"])
267
342
  output.parent.mkdir(parents=True, exist_ok=True)
268
343
  atomic_write(output, pdf)
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdview-cli
3
+ Version: 1.0.0
4
+ Summary: Iterate on Markdown and verify every revision with mdview.io
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://mdview.io
7
+ Project-URL: Repository, https://github.com/gogainda/mdview-cli
8
+ Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: click>=8.1
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: platformdirs>=4.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=8.0; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # mdv — preview Markdown files from your terminal
20
+
21
+ `mdv` renders a Markdown file the way it should look — headings, tables, code,
22
+ Mermaid diagrams — and opens it in your browser. One command, no account, no
23
+ configuration.
24
+
25
+ ```bash
26
+ uv tool install mdview-cli # or: pipx install mdview-cli, pip install mdview-cli
27
+ mdv notes.md
28
+ ```
29
+
30
+ Homebrew users:
31
+
32
+ ```bash
33
+ brew install gogainda/tap/mdview-cli
34
+ ```
35
+
36
+ That's the whole workflow: `mdv FILE.md` publishes the file to
37
+ [mdview.io](https://mdview.io), opens the rendered page, and prints a link you
38
+ can send to anyone. Anonymous previews last 30 days and take files up to 2 MB.
39
+
40
+ ## Two ways to preview
41
+
42
+ **Shareable preview (default).** `mdv FILE.md` is shorthand for `mdv preview
43
+ FILE.md`. The title comes from the first `#` heading, or pass your own:
44
+
45
+ ```bash
46
+ mdv notes.md
47
+ mdv preview notes.md --title "Design notes"
48
+ mdv preview notes.md --json # print the share URLs as JSON, don't open a browser
49
+ ```
50
+
51
+ **Local-only preview.** `mdv open` uploads nothing: it packs the file into the
52
+ URL itself and rendering happens entirely in your browser. Use it for drafts
53
+ that shouldn't leave your machine.
54
+
55
+ ```bash
56
+ mdv open notes.md
57
+ ```
58
+
59
+ ## Iterating on one document
60
+
61
+ Previews are throwaway: every run mints a new link. When you keep working on
62
+ the same file — a spec, an architecture doc, a report — you want the opposite:
63
+ one stable URL whose content follows your edits, and a check that every
64
+ revision still renders cleanly.
65
+
66
+ Set up a free mdview.io CLI token once, then it's one command per iteration:
67
+
68
+ ```bash
69
+ mdv keys set # once (or set MDVIEW_TOKEN in CI)
70
+ mdv sync spec.md # publish this revision, verify it, auto-repair broken diagrams
71
+ ```
72
+
73
+ While you iterate, the document lives at its **private** `/p/` page — only
74
+ you, signed in, can see it, and the URL stays the same forever. Every sync
75
+ shows the fixed, renderable version there. Broken Mermaid diagrams are
76
+ repaired automatically and the fix is written back into your file (the
77
+ original is kept as a timestamped backup); only documents that can't be
78
+ repaired exit `1`.
79
+
80
+ When — and only when — you decide to publish, make it public; and grab a PDF
81
+ at any point:
82
+
83
+ ```bash
84
+ mdv share spec.md # mint the public /s/ link (from then on it follows your syncs)
85
+ mdv export spec.md # render the current revision to spec.pdf
86
+ ```
87
+
88
+ Useful around the loop:
89
+
90
+ ```bash
91
+ mdv sync spec.md --no-fix # fast sync: skip auto-repair, just report failures
92
+ mdv export spec.md --no-sync # PDF of the last synced revision; local edits stay private
93
+ mdv list # all saved documents with their URLs
94
+ mdv unlink spec.md # forget the file↔document link; the document stays online
95
+ ```
96
+
97
+ Nothing is public until you run `mdv share`. After that, the public page
98
+ serves the last synced revision — mid-edit local changes still stay private
99
+ until you sync. `mdv fix FILE --local` repairs a file in place without
100
+ creating a saved document (free accounts share the daily Quick Fix limit;
101
+ Pro is unlimited).
102
+
103
+ ### Example session
104
+
105
+ A real session, working on a doc whose Mermaid diagrams start out broken:
106
+
107
+ ```console
108
+ $ mdv sync spec.md # iterate — the broken flowchart is auto-repaired (~30s)
109
+ Document: 8be0be9198400443
110
+ Private: https://mdview.io/p/8be0be9198400443
111
+ Renderable: yes (1 diagrams, 0 tables)
112
+
113
+ $ vim spec.md # …add two sections, each with a (broken) diagram…
114
+ $ mdv sync spec.md # same doc, all three diagrams repaired and verified
115
+ Document: 8be0be9198400443
116
+ Private: https://mdview.io/p/8be0be9198400443
117
+ Renderable: yes (3 diagrams, 0 tables)
118
+
119
+ $ mdv share spec.md # happy with it — make it public
120
+ https://mdview.io/s/9a2b8fb1
121
+
122
+ $ mdv export spec.md # …and hand over a PDF
123
+ PDF: spec.pdf
124
+ ```
125
+
126
+ Two commands did all the work: `mdv sync` after every editing round, and
127
+ `mdv share` once at the end. The fixes were written back into `spec.md`
128
+ (originals in timestamped backups, so you can review what the repair
129
+ changed — worth a glance before sharing, since a repair is guaranteed to
130
+ render but not to read your mind), the private URL never changed, and the
131
+ public link didn't exist until it was asked for.
132
+
133
+ **When this flow fits:** a document that lives longer than one paste — specs,
134
+ architecture docs, reports, runbooks, agent-written docs you refine over
135
+ hours or days. Use it when reviewers follow one link that must always show
136
+ the latest good revision, when a PDF has to be producible at any moment, or
137
+ in CI as a docs gate (`mdv sync --no-fix` exits `1` on broken diagrams
138
+ without editing anything). For a one-off snapshot, `mdv preview` is enough;
139
+ for reading a local file without uploading it, use `mdv open`.
140
+
141
+ ## Reference
142
+
143
+ | Command | Token | What it does |
144
+ |---|---|---|
145
+ | `mdv FILE.md` / `mdv preview` | no | Publish anonymously, open the rendered page (30-day link) |
146
+ | `mdv open` | no | Render locally in the browser; nothing is uploaded |
147
+ | `mdv sync` | yes | Update a saved document at its private /p/ URL, verify + auto-repair |
148
+ | `mdv share` | yes | Make a synced document public at its /s/ URL |
149
+ | `mdv verify` | yes | Re-check rendering of a synced document |
150
+ | `mdv fix` | yes | Repair broken Mermaid diagrams, with a local backup |
151
+ | `mdv export` | yes | Export a synced document to PDF |
152
+ | `mdv list` | yes | List saved documents |
153
+ | `mdv unlink` | no | Remove the local file↔document association |
154
+ | `mdv keys set/list/path/unset` | — | Manage the stored CLI token |
155
+
156
+ Exit codes: `0` success, `1` document doesn't render cleanly, `2` usage error,
157
+ `3` mdview.io error.
158
+
159
+ Environment: `MDVIEW_TOKEN` supplies the token without storing it (ideal for
160
+ CI); `MDVIEW_BASE_URL` points the CLI at another server.
@@ -1,6 +1,8 @@
1
+ .gitignore
1
2
  LICENSE
2
3
  README.md
3
4
  pyproject.toml
5
+ .github/workflows/release.yml
4
6
  src/mdview_cli/__init__.py
5
7
  src/mdview_cli/api.py
6
8
  src/mdview_cli/cli.py
@@ -11,5 +13,8 @@ src/mdview_cli.egg-info/SOURCES.txt
11
13
  src/mdview_cli.egg-info/dependency_links.txt
12
14
  src/mdview_cli.egg-info/entry_points.txt
13
15
  src/mdview_cli.egg-info/requires.txt
16
+ src/mdview_cli.egg-info/scm_file_list.json
17
+ src/mdview_cli.egg-info/scm_version.json
14
18
  src/mdview_cli.egg-info/top_level.txt
15
- tests/test_cli.py
19
+ tests/test_cli.py
20
+ tests/e2e/test_free_tools.py
@@ -0,0 +1,16 @@
1
+ {
2
+ "files": [
3
+ ".gitignore",
4
+ "README.md",
5
+ "pyproject.toml",
6
+ "LICENSE",
7
+ "src/mdview_cli/api.py",
8
+ "src/mdview_cli/state.py",
9
+ "src/mdview_cli/config.py",
10
+ "src/mdview_cli/cli.py",
11
+ "src/mdview_cli/__init__.py",
12
+ ".github/workflows/release.yml",
13
+ "tests/test_cli.py",
14
+ "tests/e2e/test_free_tools.py"
15
+ ]
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0.0",
3
+ "distance": 0,
4
+ "node": "g3c5d7f760b99e8bc0a5b93e1cf747fd01ac52421",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-18"
8
+ }
@@ -0,0 +1,75 @@
1
+ import base64
2
+ import os
3
+ import shutil
4
+ import subprocess
5
+ import zipfile
6
+ from io import BytesIO
7
+ from pathlib import Path
8
+
9
+
10
+ def run_mdv(*arguments, env):
11
+ executable = shutil.which("mdv")
12
+ assert executable, "mdv must be installed before running E2E tests"
13
+ return subprocess.run(
14
+ [executable, *map(str, arguments)],
15
+ env=env,
16
+ check=False,
17
+ capture_output=True,
18
+ text=True,
19
+ )
20
+
21
+
22
+ def token_free_env(tmp_path):
23
+ env = os.environ.copy()
24
+ env.pop("MDVIEW_TOKEN", None)
25
+ env.pop("MDVIEW_BASE_URL", None)
26
+ env["MDVIEW_CONFIG_DIR"] = str(tmp_path / "config")
27
+ env["MDVIEW_DATA_DIR"] = str(tmp_path / "data")
28
+ return env
29
+
30
+
31
+ def test_open_hands_the_exact_markdown_to_the_browser_without_a_token(tmp_path):
32
+ markdown = "# Architecture\n\n```mermaid\nflowchart LR\n A --> B\n```\n"
33
+ document = tmp_path / "architecture.md"
34
+ document.write_text(markdown, encoding="utf-8")
35
+ captured_url = tmp_path / "browser-url.txt"
36
+ browser = tmp_path / "capture-browser"
37
+ browser.write_text(
38
+ "#!/usr/bin/env python3\n"
39
+ "import os, pathlib, sys\n"
40
+ "pathlib.Path(os.environ['MDV_CAPTURE_URL']).write_text(sys.argv[1])\n",
41
+ encoding="utf-8",
42
+ )
43
+ browser.chmod(0o755)
44
+ env = token_free_env(tmp_path)
45
+ env["BROWSER"] = str(browser)
46
+ env["MDV_CAPTURE_URL"] = str(captured_url)
47
+
48
+ result = run_mdv("open", document, env=env)
49
+
50
+ assert result.returncode == 0, result.stderr
51
+ assert f"Opened {document}" in result.stdout
52
+ url = captured_url.read_text(encoding="utf-8")
53
+ prefix = "https://mdview.io/#mvd=zip:"
54
+ assert url.startswith(prefix)
55
+ archive = zipfile.ZipFile(BytesIO(base64.b64decode(url.removeprefix(prefix))))
56
+ assert archive.namelist() == ["architecture.md"]
57
+ assert archive.read("architecture.md").decode("utf-8") == markdown
58
+
59
+
60
+ def test_help_version_and_key_inspection_work_without_a_token(tmp_path):
61
+ env = token_free_env(tmp_path)
62
+
63
+ help_result = run_mdv("--help", env=env)
64
+ version_result = run_mdv("--version", env=env)
65
+ keys_result = run_mdv("keys", "list", env=env)
66
+ path_result = run_mdv("keys", "path", env=env)
67
+
68
+ assert help_result.returncode == 0
69
+ assert "open" in help_result.stdout
70
+ assert version_result.returncode == 0
71
+ assert version_result.stdout.startswith("mdv, version ")
72
+ assert keys_result.returncode == 0
73
+ assert keys_result.stdout.strip() == "No token configured."
74
+ assert path_result.returncode == 0
75
+ assert Path(path_result.stdout.strip()) == tmp_path / "config" / "keys.json"
@@ -13,6 +13,7 @@ class FakeApi:
13
13
  self.created = 0
14
14
  self.updated = 0
15
15
  self.verified = 0
16
+ self.shared = 0
16
17
  self.missing_on_update = False
17
18
 
18
19
  def create(self, title, content):
@@ -27,6 +28,7 @@ class FakeApi:
27
28
  return {"ok": True, "updated_at": "two"}
28
29
 
29
30
  def share(self, document_id):
31
+ self.shared += 1
30
32
  return {"shortId": "share123"}
31
33
 
32
34
  def verify(self, document_id, status=False):
@@ -45,6 +47,16 @@ class FakeApi:
45
47
  self.renderable = True
46
48
  return {"markdown": "# Fixed\n\n```mermaid\ngraph TD; A-->B\n```\n", "diagrams": {"fixed": 1}}
47
49
 
50
+ def export_pdf(self, document_id):
51
+ return b"%PDF-1.4 rendered"
52
+
53
+ def fix_markdown(self, content):
54
+ return {
55
+ "renderable": True,
56
+ "markdown": "# Fixed locally\n",
57
+ "diagrams": {"total": 1, "failing_before": 1, "fixed": 1, "remaining": 0},
58
+ }
59
+
48
60
 
49
61
  def configure(monkeypatch, tmp_path, api):
50
62
  monkeypatch.setenv("MDVIEW_TOKEN", "mdv1_test")
@@ -66,14 +78,20 @@ def test_sync_creates_once_then_updates_and_always_verifies(monkeypatch, tmp_pat
66
78
 
67
79
  assert first.exit_code == 0
68
80
  assert second.exit_code == 0
69
- assert "https://mdview.io/s/share123" in second.output
81
+ assert "https://mdview.io/p/doc123" in second.output
70
82
  assert api.created == 1
71
83
  assert api.updated == 1
72
84
  assert api.verified == 2
85
+ assert api.shared == 0
73
86
 
74
87
 
75
- def test_sync_reports_broken_diagram_and_exits_one(monkeypatch, tmp_path):
88
+ def test_sync_reports_broken_diagram_and_exits_one_when_repair_unavailable(monkeypatch, tmp_path):
76
89
  api = FakeApi(renderable=False)
90
+
91
+ def quota_exhausted(document_id):
92
+ raise ApiError("Daily Quick Fix limit reached. Upgrade to Pro for unlimited fixes.", 402)
93
+
94
+ api.fix = quota_exhausted
77
95
  configure(monkeypatch, tmp_path, api)
78
96
  document = tmp_path / "broken.md"
79
97
  document.write_text("# Broken\n", encoding="utf-8")
@@ -81,8 +99,8 @@ def test_sync_reports_broken_diagram_and_exits_one(monkeypatch, tmp_path):
81
99
  result = CliRunner().invoke(cli, ["sync", str(document)])
82
100
 
83
101
  assert result.exit_code == 1
102
+ assert "Auto-repair unavailable" in result.output
84
103
  assert "Diagram 0: Parse error" in result.output
85
- assert "mdv fix FILE" in result.output
86
104
 
87
105
 
88
106
  def test_sync_recreates_a_deleted_associated_document(monkeypatch, tmp_path):
@@ -168,6 +186,86 @@ def test_unknown_command_still_errors(tmp_path):
168
186
  assert "No such command" in result.output
169
187
 
170
188
 
189
+ def test_share_publishes_on_request_and_sync_stays_private_until_then(monkeypatch, tmp_path):
190
+ api = FakeApi()
191
+ configure(monkeypatch, tmp_path, api)
192
+ document = tmp_path / "doc.md"
193
+ document.write_text("# Doc\n", encoding="utf-8")
194
+ runner = CliRunner()
195
+ assert runner.invoke(cli, ["sync", str(document)]).exit_code == 0
196
+ assert api.shared == 0
197
+
198
+ shared = runner.invoke(cli, ["share", str(document)])
199
+ resynced = runner.invoke(cli, ["sync", str(document)])
200
+
201
+ assert shared.exit_code == 0
202
+ assert shared.output.strip() == "https://mdview.io/s/share123"
203
+ assert api.shared == 1
204
+ assert "Share: https://mdview.io/s/share123" in resynced.output
205
+
206
+
207
+ def test_sync_no_fix_skips_repair_and_exits_one(monkeypatch, tmp_path):
208
+ api = FakeApi(renderable=False)
209
+ configure(monkeypatch, tmp_path, api)
210
+ document = tmp_path / "broken.md"
211
+ original = "# Broken\n\n```mermaid\nnope\n```\n"
212
+ document.write_text(original, encoding="utf-8")
213
+
214
+ result = CliRunner().invoke(cli, ["sync", str(document), "--no-fix"])
215
+
216
+ assert result.exit_code == 1
217
+ assert "Diagram 0: Parse error" in result.output
218
+ assert document.read_text(encoding="utf-8") == original
219
+
220
+
221
+ def test_sync_auto_repairs_broken_diagrams_and_exits_zero(monkeypatch, tmp_path):
222
+ api = FakeApi(renderable=False)
223
+ configure(monkeypatch, tmp_path, api)
224
+ document = tmp_path / "broken.md"
225
+ document.write_text("# Broken\n\n```mermaid\nnope\n```\n", encoding="utf-8")
226
+
227
+ result = CliRunner().invoke(cli, ["sync", str(document)])
228
+
229
+ assert result.exit_code == 0
230
+ assert document.read_text(encoding="utf-8").startswith("# Fixed")
231
+ assert "Renderable: yes" in result.output
232
+
233
+
234
+ def test_export_no_sync_exports_published_doc_without_uploading(monkeypatch, tmp_path):
235
+ api = FakeApi()
236
+ configure(monkeypatch, tmp_path, api)
237
+ document = tmp_path / "doc.md"
238
+ document.write_text("# Doc\n", encoding="utf-8")
239
+ runner = CliRunner()
240
+ assert runner.invoke(cli, ["sync", str(document)]).exit_code == 0
241
+ document.write_text("# Broken WIP edit\n", encoding="utf-8")
242
+
243
+ result = runner.invoke(cli, ["export", str(document), "--no-sync"])
244
+
245
+ assert result.exit_code == 0
246
+ assert (tmp_path / "doc.pdf").read_bytes() == b"%PDF-1.4 rendered"
247
+ assert api.created == 1 and api.updated == 0
248
+
249
+
250
+ def test_fix_local_rewrites_file_without_syncing(monkeypatch, tmp_path):
251
+ api = FakeApi()
252
+ configure(monkeypatch, tmp_path, api)
253
+ document = tmp_path / "broken.md"
254
+ original = "# Broken\n\n```mermaid\nnope\n```\n"
255
+ document.write_text(original, encoding="utf-8")
256
+
257
+ result = CliRunner().invoke(cli, ["fix", str(document), "--local", "--json"])
258
+
259
+ assert result.exit_code == 0
260
+ payload = json.loads(result.output)
261
+ assert payload["fixed"] == 1
262
+ assert document.read_text(encoding="utf-8") == "# Fixed locally\n"
263
+ assert Path(payload["backup"]).read_text(encoding="utf-8") == original
264
+ assert api.created == 0
265
+ assert api.updated == 0
266
+ assert api.verified == 0
267
+
268
+
171
269
  def test_fix_backs_up_rewrites_resyncs_and_verifies(monkeypatch, tmp_path):
172
270
  api = FakeApi(renderable=False)
173
271
  configure(monkeypatch, tmp_path, api)
mdview_cli-0.1.0/PKG-INFO DELETED
@@ -1,63 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mdview-cli
3
- Version: 0.1.0
4
- Summary: Iterate on Markdown and verify every revision with mdview.io
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://mdview.io
7
- Project-URL: Repository, https://github.com/gogainda/mdview-cli
8
- Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: click>=8.1
13
- Requires-Dist: httpx>=0.27
14
- Requires-Dist: platformdirs>=4.0
15
- Provides-Extra: test
16
- Requires-Dist: pytest>=8.0; extra == "test"
17
- Dynamic: license-file
18
-
19
- # mdv
20
-
21
- `mdv` opens Markdown in mdview.io and can keep an evolving file attached to one
22
- stable document while checking every revision in the PDF renderer.
23
-
24
- ## Quick start — no token required
25
-
26
- ```bash
27
- uv tool install mdview-cli
28
- mdv architecture.md
29
- ```
30
-
31
- `mdv FILE.md` is shorthand for `mdv preview FILE.md`: it publishes the file
32
- anonymously and opens a temporary mdview page you can share (30-day expiry,
33
- 2 MB limit). No account, token, or configuration required.
34
-
35
- `open` renders a local file in the browser without uploading anything at all:
36
-
37
- ```bash
38
- mdv open architecture.md
39
- ```
40
-
41
- ## Verify every revision
42
-
43
- Saved-document commands require a mdview.io CLI token because they update your
44
- documents between runs. Configure it once, then sync the same file after every
45
- edit:
46
-
47
- ```bash
48
- mdv keys set
49
- mdv sync architecture.md
50
- ```
51
-
52
- Edit the file and run `mdv sync architecture.md` again. The share URL stays the
53
- same. Exit status `0` means every Mermaid diagram rendered; status `1` means the
54
- document needs attention.
55
-
56
- ```bash
57
- mdv fix architecture.md
58
- mdv export architecture.md
59
- ```
60
-
61
- `open` and `preview` are token-free. `sync`, `verify`, `fix`, `export`, and `list` use a token
62
- to access Saved documents. In CI, set `MDVIEW_TOKEN` instead of storing it.
63
- `MDVIEW_BASE_URL` overrides the service URL for development.
@@ -1,45 +0,0 @@
1
- # mdv
2
-
3
- `mdv` opens Markdown in mdview.io and can keep an evolving file attached to one
4
- stable document while checking every revision in the PDF renderer.
5
-
6
- ## Quick start — no token required
7
-
8
- ```bash
9
- uv tool install mdview-cli
10
- mdv architecture.md
11
- ```
12
-
13
- `mdv FILE.md` is shorthand for `mdv preview FILE.md`: it publishes the file
14
- anonymously and opens a temporary mdview page you can share (30-day expiry,
15
- 2 MB limit). No account, token, or configuration required.
16
-
17
- `open` renders a local file in the browser without uploading anything at all:
18
-
19
- ```bash
20
- mdv open architecture.md
21
- ```
22
-
23
- ## Verify every revision
24
-
25
- Saved-document commands require a mdview.io CLI token because they update your
26
- documents between runs. Configure it once, then sync the same file after every
27
- edit:
28
-
29
- ```bash
30
- mdv keys set
31
- mdv sync architecture.md
32
- ```
33
-
34
- Edit the file and run `mdv sync architecture.md` again. The share URL stays the
35
- same. Exit status `0` means every Mermaid diagram rendered; status `1` means the
36
- document needs attention.
37
-
38
- ```bash
39
- mdv fix architecture.md
40
- mdv export architecture.md
41
- ```
42
-
43
- `open` and `preview` are token-free. `sync`, `verify`, `fix`, `export`, and `list` use a token
44
- to access Saved documents. In CI, set `MDVIEW_TOKEN` instead of storing it.
45
- `MDVIEW_BASE_URL` overrides the service URL for development.
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
@@ -1,63 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mdview-cli
3
- Version: 0.1.0
4
- Summary: Iterate on Markdown and verify every revision with mdview.io
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://mdview.io
7
- Project-URL: Repository, https://github.com/gogainda/mdview-cli
8
- Project-URL: Issues, https://github.com/gogainda/mdview-cli/issues
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: click>=8.1
13
- Requires-Dist: httpx>=0.27
14
- Requires-Dist: platformdirs>=4.0
15
- Provides-Extra: test
16
- Requires-Dist: pytest>=8.0; extra == "test"
17
- Dynamic: license-file
18
-
19
- # mdv
20
-
21
- `mdv` opens Markdown in mdview.io and can keep an evolving file attached to one
22
- stable document while checking every revision in the PDF renderer.
23
-
24
- ## Quick start — no token required
25
-
26
- ```bash
27
- uv tool install mdview-cli
28
- mdv architecture.md
29
- ```
30
-
31
- `mdv FILE.md` is shorthand for `mdv preview FILE.md`: it publishes the file
32
- anonymously and opens a temporary mdview page you can share (30-day expiry,
33
- 2 MB limit). No account, token, or configuration required.
34
-
35
- `open` renders a local file in the browser without uploading anything at all:
36
-
37
- ```bash
38
- mdv open architecture.md
39
- ```
40
-
41
- ## Verify every revision
42
-
43
- Saved-document commands require a mdview.io CLI token because they update your
44
- documents between runs. Configure it once, then sync the same file after every
45
- edit:
46
-
47
- ```bash
48
- mdv keys set
49
- mdv sync architecture.md
50
- ```
51
-
52
- Edit the file and run `mdv sync architecture.md` again. The share URL stays the
53
- same. Exit status `0` means every Mermaid diagram rendered; status `1` means the
54
- document needs attention.
55
-
56
- ```bash
57
- mdv fix architecture.md
58
- mdv export architecture.md
59
- ```
60
-
61
- `open` and `preview` are token-free. `sync`, `verify`, `fix`, `export`, and `list` use a token
62
- to access Saved documents. In CI, set `MDVIEW_TOKEN` instead of storing it.
63
- `MDVIEW_BASE_URL` overrides the service URL for development.
File without changes
File without changes