mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-05-07 06:36:34 +08:00
Add Mermaid Live Editor harness
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -33,6 +33,7 @@
|
||||
!/anygen/
|
||||
!/zoom/
|
||||
!/drawio/
|
||||
!/mermaid/
|
||||
|
||||
# Step 5: Inside each software dir, ignore everything (including dotfiles)
|
||||
/gimp/*
|
||||
@@ -57,6 +58,8 @@
|
||||
/zoom/.*
|
||||
/drawio/*
|
||||
/drawio/.*
|
||||
/mermaid/*
|
||||
/mermaid/.*
|
||||
|
||||
# Step 6: ...except agent-harness/
|
||||
!/gimp/agent-harness/
|
||||
@@ -70,6 +73,7 @@
|
||||
!/anygen/agent-harness/
|
||||
!/zoom/agent-harness/
|
||||
!/drawio/agent-harness/
|
||||
!/mermaid/agent-harness/
|
||||
|
||||
# Step 7: Ignore build artifacts within allowed dirs
|
||||
**/__pycache__/
|
||||
|
||||
25
README.md
25
README.md
@@ -7,7 +7,7 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software</stron
|
||||
|
||||
<p align="center">
|
||||
<a href="#-quick-start"><img src="https://img.shields.io/badge/Quick_Start-5_min-blue?style=for-the-badge" alt="Quick Start"></a>
|
||||
<a href="#-demonstrations"><img src="https://img.shields.io/badge/Demos-11_Apps-green?style=for-the-badge" alt="Demos"></a>
|
||||
<a href="#-demonstrations"><img src="https://img.shields.io/badge/Demos-12_Apps-green?style=for-the-badge" alt="Demos"></a>
|
||||
<a href="#-test-results"><img src="https://img.shields.io/badge/Tests-1%2C508_Passing-brightgreen?style=for-the-badge" alt="Tests"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="License"></a>
|
||||
</p>
|
||||
@@ -361,7 +361,7 @@ AI agents are great at reasoning but terrible at using real professional softwar
|
||||
| 💸 "UI automation breaks constantly" | No screenshots, no clicking, no RPA fragility. Pure command-line reliability with structured interfaces |
|
||||
| 📊 "Agents need structured data" | Built-in JSON output for seamless agent consumption + human-readable formats for debugging |
|
||||
| 🔧 "Custom integrations are expensive" | One Claude plugin auto-generates CLIs for ANY codebase through proven 7-phase pipeline |
|
||||
| ⚡ "Prototype vs Production gap" | 1,508+ tests with real software validation. Battle-tested across 11 major applications |
|
||||
| ⚡ "Prototype vs Production gap" | 1,518+ tests with real software validation. Battle-tested across 12 major applications |
|
||||
|
||||
---
|
||||
|
||||
@@ -450,7 +450,7 @@ All CLIs organized under cli_anything.* namespace — conflict-free, pip-install
|
||||
CLI-Anything works on any software with a codebase — no domain restrictions or architectural limitations.
|
||||
|
||||
### 🏭 Professional-Grade Testing
|
||||
Tested across 11 diverse, complex applications spanning creative, productivity, communication, diagramming, and AI content generation domains previously inaccessible to AI agents.
|
||||
Tested across 12 diverse, complex applications spanning creative, productivity, communication, diagramming, and AI content generation domains previously inaccessible to AI agents.
|
||||
|
||||
### 🎨 Diverse Domain Coverage
|
||||
From creative workflows (image editing, 3D modeling, vector graphics) to production tools (audio, office, live streaming, video editing).
|
||||
@@ -537,6 +537,13 @@ Each application received complete, production-ready CLI interfaces — not demo
|
||||
<td align="center">✅ 138</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><strong>🧜 Mermaid Live Editor</strong></td>
|
||||
<td>Diagramming</td>
|
||||
<td><code>cli-anything-mermaid</code></td>
|
||||
<td>Mermaid state + mermaid.ink renderer</td>
|
||||
<td align="center">✅ 10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><strong>✨ AnyGen</strong></td>
|
||||
<td>AI Content Generation</td>
|
||||
<td><code>cli-anything-anygen</code></td>
|
||||
@@ -545,11 +552,11 @@ Each application received complete, production-ready CLI interfaces — not demo
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="4"><strong>Total</strong></td>
|
||||
<td align="center"><strong>✅ 1,508</strong></td>
|
||||
<td align="center"><strong>✅ 1,518</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> **100% pass rate** across all 1,508 tests — 1,073 unit tests + 435 end-to-end tests.
|
||||
> **100% pass rate** across all 1,518 tests — 1,078 unit tests + 440 end-to-end tests.
|
||||
|
||||
---
|
||||
|
||||
@@ -576,9 +583,10 @@ kdenlive 155 passed ✅ (111 unit + 44 e2e)
|
||||
shotcut 154 passed ✅ (110 unit + 44 e2e)
|
||||
zoom 22 passed ✅ (22 unit + 0 e2e)
|
||||
drawio 138 passed ✅ (116 unit + 22 e2e)
|
||||
mermaid 10 passed ✅ (5 unit + 5 e2e)
|
||||
anygen 50 passed ✅ (40 unit + 10 e2e)
|
||||
──────────────────────────────────────────────────────────────────────────────
|
||||
TOTAL 1,508 passed ✅ 100% pass rate
|
||||
TOTAL 1,518 passed ✅ 100% pass rate
|
||||
```
|
||||
|
||||
---
|
||||
@@ -637,6 +645,7 @@ cli-anything/
|
||||
├── 🎬 shotcut/agent-harness/ # Shotcut CLI (154 tests)
|
||||
├── 📞 zoom/agent-harness/ # Zoom CLI (22 tests)
|
||||
├── 📐 drawio/agent-harness/ # Draw.io CLI (138 tests)
|
||||
├── 🧜 mermaid/agent-harness/ # Mermaid Live Editor CLI (10 tests)
|
||||
└── ✨ anygen/agent-harness/ # AnyGen CLI (50 tests)
|
||||
```
|
||||
|
||||
@@ -738,7 +747,7 @@ HARNESS.md is our definitive SOP for making any software agent-accessible via au
|
||||
|
||||
It encodes proven patterns and methodologies refined through automated generation processes.
|
||||
|
||||
The playbook distills key insights from successfully building all 11 diverse, production-ready harnesses.
|
||||
The playbook distills key insights from successfully building all 12 diverse, production-ready harnesses.
|
||||
|
||||
### Critical Lessons
|
||||
|
||||
@@ -863,7 +872,7 @@ MIT License — free to use, modify, and distribute.
|
||||
|
||||
**CLI-Anything** — *Make any software with a codebase Agent-native.*
|
||||
|
||||
<sub>A methodology for the age of AI agents | 11 professional software demos | 1,508 passing tests</sub>
|
||||
<sub>A methodology for the age of AI agents | 12 professional software demos | 1,518 passing tests</sub>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
60
mermaid/agent-harness/MERMAID.md
Normal file
60
mermaid/agent-harness/MERMAID.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Mermaid Live Editor - CLI Harness Analysis
|
||||
|
||||
## Software Overview
|
||||
|
||||
**Mermaid Live Editor** is the official browser editor for Mermaid diagrams. It edits Mermaid source text, previews diagrams in real time, generates shareable URLs, and renders diagrams through the Mermaid renderer service.
|
||||
|
||||
## Architecture
|
||||
|
||||
### State model
|
||||
|
||||
The live editor centers around a serialized state object. The fields relevant to the CLI are:
|
||||
|
||||
- `code`: Mermaid diagram source
|
||||
- `mermaid`: Mermaid config JSON string
|
||||
- `updateDiagram`: whether the view should refresh
|
||||
- `rough`: rough-sketch rendering toggle
|
||||
- `panZoom`: pan/zoom enablement
|
||||
- `grid`: editor grid toggle
|
||||
|
||||
For the CLI harness, this state is stored as a JSON project file with a `.mermaid.json` suffix.
|
||||
|
||||
### Native share/render format
|
||||
|
||||
The live editor serializes its state as:
|
||||
|
||||
1. compact JSON
|
||||
2. zlib compression
|
||||
3. URL-safe base64
|
||||
4. `pako:` prefix
|
||||
|
||||
That serialized token is used by the official endpoints:
|
||||
|
||||
- Edit URL: `https://mermaid.live/edit#<serialized>`
|
||||
- View URL: `https://mermaid.live/view#<serialized>`
|
||||
- SVG URL: `https://mermaid.ink/svg/<serialized>`
|
||||
- PNG URL: `https://mermaid.ink/img/<serialized>?type=png`
|
||||
|
||||
## Backend strategy
|
||||
|
||||
This harness uses the same renderer path that Mermaid Live Editor uses in production. The backend module generates the same serialized state payload and invokes the official Mermaid renderer endpoint.
|
||||
|
||||
That keeps the harness aligned with the actual application instead of inventing a parallel diagram format.
|
||||
|
||||
## CLI scope
|
||||
|
||||
- `project new/open/save/info/samples`
|
||||
- `diagram set/show`
|
||||
- `export render/share`
|
||||
- `session status/undo/redo`
|
||||
- REPL mode by default
|
||||
|
||||
## Expected validation
|
||||
|
||||
The CLI should prove that an agent can:
|
||||
|
||||
1. create a Mermaid project
|
||||
2. update diagram text
|
||||
3. inspect current source
|
||||
4. generate live-editor share/view URLs
|
||||
5. render a real SVG and PNG artifact
|
||||
71
mermaid/agent-harness/cli_anything/mermaid/README.md
Normal file
71
mermaid/agent-harness/cli_anything/mermaid/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# cli-anything-mermaid
|
||||
|
||||
A CLI harness for **Mermaid Live Editor** that lets agents create Mermaid state files, inspect diagram source, generate share URLs, and render diagrams through the official Mermaid renderer path.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- Internet access to `https://mermaid.live` and `https://mermaid.ink`, or a compatible self-hosted Mermaid renderer service
|
||||
|
||||
The upstream Mermaid Live Editor defaults to `mermaid.ink` for render links. This harness uses the same serialized state format and endpoint shape.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd mermaid/agent-harness
|
||||
python -m pip install -e .[dev]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### One-shot commands
|
||||
|
||||
```bash
|
||||
# Create a new Mermaid project
|
||||
cli-anything-mermaid project new --sample flowchart -o diagram.mermaid.json
|
||||
|
||||
# Replace the current diagram source
|
||||
cli-anything-mermaid --project diagram.mermaid.json diagram set --text "graph TD; A[Test] --> B[Works]"
|
||||
|
||||
# Show a shareable URL
|
||||
cli-anything-mermaid --project diagram.mermaid.json export share --mode view
|
||||
|
||||
# Render via the Mermaid renderer backend
|
||||
cli-anything-mermaid --project diagram.mermaid.json export render output.svg -f svg --overwrite
|
||||
cli-anything-mermaid --project diagram.mermaid.json export render output.png -f png --overwrite
|
||||
```
|
||||
|
||||
### JSON mode
|
||||
|
||||
```bash
|
||||
cli-anything-mermaid --json project new -o diagram.mermaid.json
|
||||
cli-anything-mermaid --json --project diagram.mermaid.json export share --mode edit
|
||||
cli-anything-mermaid --json --project diagram.mermaid.json export render output.svg -f svg --overwrite
|
||||
```
|
||||
|
||||
### Interactive REPL
|
||||
|
||||
```bash
|
||||
cli-anything-mermaid
|
||||
cli-anything-mermaid --project diagram.mermaid.json
|
||||
```
|
||||
|
||||
## Command reference
|
||||
|
||||
- `project new/open/save/info/samples`
|
||||
- `diagram set/show`
|
||||
- `export render/share`
|
||||
- `session status/undo/redo`
|
||||
|
||||
## Notes
|
||||
|
||||
- Project files are stored as `.mermaid.json`
|
||||
- Rendered output is produced through the same serialized state payload the live editor uses
|
||||
- `export share` emits URLs that open in Mermaid Live Editor
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
cd mermaid/agent-harness
|
||||
python -m pytest cli_anything/mermaid/tests/ -v --tb=no
|
||||
```
|
||||
2
mermaid/agent-harness/cli_anything/mermaid/__init__.py
Normal file
2
mermaid/agent-harness/cli_anything/mermaid/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Mermaid CLI harness package."""
|
||||
|
||||
5
mermaid/agent-harness/cli_anything/mermaid/__main__.py
Normal file
5
mermaid/agent-harness/cli_anything/mermaid/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .mermaid_cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Core Mermaid CLI modules."""
|
||||
|
||||
24
mermaid/agent-harness/cli_anything/mermaid/core/diagram.py
Normal file
24
mermaid/agent-harness/cli_anything/mermaid/core/diagram.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Diagram text operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .session import Session
|
||||
|
||||
|
||||
def set_diagram(session: Session, text: str) -> dict:
|
||||
if not session.is_open:
|
||||
raise RuntimeError("No project is open")
|
||||
session.set_code(text)
|
||||
return {
|
||||
"action": "set_diagram",
|
||||
"line_count": len(text.splitlines()),
|
||||
}
|
||||
|
||||
|
||||
def show_diagram(session: Session) -> dict:
|
||||
if not session.is_open or session.state is None:
|
||||
raise RuntimeError("No project is open")
|
||||
return {
|
||||
"action": "show_diagram",
|
||||
"code": session.state["code"],
|
||||
}
|
||||
30
mermaid/agent-harness/cli_anything/mermaid/core/export.py
Normal file
30
mermaid/agent-harness/cli_anything/mermaid/core/export.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Render and share operations for Mermaid Live Editor state."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from .session import Session
|
||||
from ..utils import mermaid_backend
|
||||
|
||||
|
||||
def render(session: Session, output_path: str, fmt: str = "svg", overwrite: bool = False) -> dict:
|
||||
if not session.is_open or session.state is None:
|
||||
raise RuntimeError("No project is open")
|
||||
if os.path.exists(output_path) and not overwrite:
|
||||
raise FileExistsError(f"Output already exists: {output_path}")
|
||||
serialized = mermaid_backend.serialize_state(session.state)
|
||||
result = mermaid_backend.render_to_file(serialized, output_path, fmt)
|
||||
result["action"] = "render"
|
||||
return result
|
||||
|
||||
|
||||
def share(session: Session, mode: str = "edit") -> dict:
|
||||
if not session.is_open or session.state is None:
|
||||
raise RuntimeError("No project is open")
|
||||
serialized = mermaid_backend.serialize_state(session.state)
|
||||
return {
|
||||
"action": "share",
|
||||
"mode": mode,
|
||||
"url": mermaid_backend.build_live_url(serialized, mode),
|
||||
}
|
||||
47
mermaid/agent-harness/cli_anything/mermaid/core/project.py
Normal file
47
mermaid/agent-harness/cli_anything/mermaid/core/project.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Project commands for Mermaid state files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .session import SAMPLE_DIAGRAMS, Session
|
||||
|
||||
|
||||
def new_project(session: Session, sample: str = "flowchart", theme: str = "default") -> dict:
|
||||
state = session.new_project(sample=sample, theme=theme)
|
||||
return {
|
||||
"action": "new_project",
|
||||
"sample": sample,
|
||||
"theme": theme,
|
||||
"line_count": len(state["code"].splitlines()),
|
||||
}
|
||||
|
||||
|
||||
def open_project(session: Session, path: str) -> dict:
|
||||
state = session.open_project(path)
|
||||
return {
|
||||
"action": "open_project",
|
||||
"path": session.project_path,
|
||||
"line_count": len(state["code"].splitlines()),
|
||||
}
|
||||
|
||||
|
||||
def save_project(session: Session, path: str | None = None) -> dict:
|
||||
saved = session.save_project(path)
|
||||
return {
|
||||
"action": "save_project",
|
||||
"path": saved,
|
||||
}
|
||||
|
||||
|
||||
def project_info(session: Session) -> dict:
|
||||
if not session.is_open or session.state is None:
|
||||
raise RuntimeError("No project is open")
|
||||
return {
|
||||
"project_path": session.project_path,
|
||||
"line_count": len(session.state["code"].splitlines()),
|
||||
"theme_json": session.state["mermaid"],
|
||||
"modified": session.modified,
|
||||
}
|
||||
|
||||
|
||||
def list_samples() -> dict:
|
||||
return SAMPLE_DIAGRAMS.copy()
|
||||
112
mermaid/agent-harness/cli_anything/mermaid/core/session.py
Normal file
112
mermaid/agent-harness/cli_anything/mermaid/core/session.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Session state for Mermaid projects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
SAMPLE_DIAGRAMS = {
|
||||
"flowchart": "flowchart TD\n A[Start] --> B{Ready?}\n B -->|Yes| C[Run test]\n B -->|No| D[Fix input]\n",
|
||||
"sequence": "sequenceDiagram\n participant U as User\n participant C as CLI\n U->>C: Run command\n C-->>U: JSON result\n",
|
||||
"er": "erDiagram\n USER ||--o{ ORDER : places\n USER {\n int id\n string email\n }\n ORDER {\n int id\n decimal total\n }\n",
|
||||
}
|
||||
|
||||
|
||||
def default_state(sample: str = "flowchart", theme: str = "default") -> dict:
|
||||
if sample not in SAMPLE_DIAGRAMS:
|
||||
raise ValueError(f"Unknown sample: {sample}")
|
||||
return {
|
||||
"code": SAMPLE_DIAGRAMS[sample],
|
||||
"mermaid": json.dumps({"theme": theme}, indent=2),
|
||||
"updateDiagram": True,
|
||||
"rough": False,
|
||||
"panZoom": True,
|
||||
"grid": True,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Session:
|
||||
state: dict | None = None
|
||||
project_path: str | None = None
|
||||
modified: bool = False
|
||||
undo_stack: list[dict] = field(default_factory=list)
|
||||
redo_stack: list[dict] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
return self.state is not None
|
||||
|
||||
def checkpoint(self) -> None:
|
||||
if self.state is None:
|
||||
return
|
||||
self.undo_stack.append(copy.deepcopy(self.state))
|
||||
self.redo_stack.clear()
|
||||
|
||||
def new_project(self, sample: str = "flowchart", theme: str = "default") -> dict:
|
||||
self.state = default_state(sample=sample, theme=theme)
|
||||
self.project_path = None
|
||||
self.modified = True
|
||||
self.undo_stack.clear()
|
||||
self.redo_stack.clear()
|
||||
return copy.deepcopy(self.state)
|
||||
|
||||
def open_project(self, path: str) -> dict:
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Project not found: {path}")
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
self.state = json.load(fh)
|
||||
self.project_path = os.path.abspath(path)
|
||||
self.modified = False
|
||||
self.undo_stack.clear()
|
||||
self.redo_stack.clear()
|
||||
return copy.deepcopy(self.state)
|
||||
|
||||
def save_project(self, path: str | None = None) -> str:
|
||||
if self.state is None:
|
||||
raise RuntimeError("No project is open")
|
||||
target = os.path.abspath(path or self.project_path or "diagram.mermaid.json")
|
||||
parent = os.path.dirname(target)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(target, "w", encoding="utf-8") as fh:
|
||||
json.dump(self.state, fh, indent=2)
|
||||
self.project_path = target
|
||||
self.modified = False
|
||||
return target
|
||||
|
||||
def set_code(self, code: str) -> None:
|
||||
if self.state is None:
|
||||
raise RuntimeError("No project is open")
|
||||
self.checkpoint()
|
||||
self.state["code"] = code
|
||||
self.state["updateDiagram"] = True
|
||||
self.modified = True
|
||||
|
||||
def status(self) -> dict:
|
||||
return {
|
||||
"project_open": self.is_open,
|
||||
"project_path": self.project_path,
|
||||
"modified": self.modified,
|
||||
"undo_depth": len(self.undo_stack),
|
||||
"redo_depth": len(self.redo_stack),
|
||||
}
|
||||
|
||||
def undo(self) -> bool:
|
||||
if self.state is None or not self.undo_stack:
|
||||
return False
|
||||
self.redo_stack.append(copy.deepcopy(self.state))
|
||||
self.state = self.undo_stack.pop()
|
||||
self.modified = True
|
||||
return True
|
||||
|
||||
def redo(self) -> bool:
|
||||
if self.state is None or not self.redo_stack:
|
||||
return False
|
||||
self.undo_stack.append(copy.deepcopy(self.state))
|
||||
self.state = self.redo_stack.pop()
|
||||
self.modified = True
|
||||
return True
|
||||
242
mermaid/agent-harness/cli_anything/mermaid/mermaid_cli.py
Normal file
242
mermaid/agent-harness/cli_anything/mermaid/mermaid_cli.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Stateful CLI harness for Mermaid Live Editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import click
|
||||
|
||||
from .core import diagram as diagram_mod
|
||||
from .core import export as export_mod
|
||||
from .core import project as project_mod
|
||||
from .core.session import Session
|
||||
from .utils.repl_skin import ReplSkin
|
||||
|
||||
|
||||
_session: Session | None = None
|
||||
_json_output = False
|
||||
|
||||
|
||||
def get_session() -> Session:
|
||||
global _session
|
||||
if _session is None:
|
||||
_session = Session()
|
||||
return _session
|
||||
|
||||
|
||||
def emit(data, message: str | None = None) -> None:
|
||||
if _json_output:
|
||||
click.echo(json.dumps(data, indent=2))
|
||||
return
|
||||
if message:
|
||||
click.echo(message)
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
click.echo(f"{key}: {value}")
|
||||
else:
|
||||
click.echo(str(data))
|
||||
|
||||
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON")
|
||||
@click.option("--project", "project_path", default=None, help="Open a Mermaid project file")
|
||||
@click.pass_context
|
||||
def cli(ctx, json_mode: bool, project_path: str | None) -> None:
|
||||
"""CLI harness for Mermaid Live Editor state files and renderer URLs."""
|
||||
global _json_output, _session
|
||||
_json_output = json_mode
|
||||
_session = Session()
|
||||
if project_path:
|
||||
_session.open_project(project_path)
|
||||
|
||||
@ctx.call_on_close
|
||||
def _auto_save() -> None:
|
||||
if project_path and _session and _session.is_open and _session.modified:
|
||||
_session.save_project()
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.invoke(repl)
|
||||
|
||||
|
||||
@cli.group()
|
||||
def project() -> None:
|
||||
"""Project lifecycle commands."""
|
||||
|
||||
|
||||
@project.command("new")
|
||||
@click.option("--sample", default="flowchart", help="Sample diagram preset")
|
||||
@click.option("--theme", default="default", help="Mermaid theme")
|
||||
@click.option("-o", "--output", "output_path", default=None, help="Optional file path to save")
|
||||
def project_new(sample: str, theme: str, output_path: str | None) -> None:
|
||||
session = get_session()
|
||||
result = project_mod.new_project(session, sample=sample, theme=theme)
|
||||
if output_path:
|
||||
saved = project_mod.save_project(session, output_path)
|
||||
result["path"] = saved["path"]
|
||||
emit(result, "Created Mermaid project")
|
||||
|
||||
|
||||
@project.command("open")
|
||||
@click.argument("path")
|
||||
def project_open(path: str) -> None:
|
||||
emit(project_mod.open_project(get_session(), path), "Opened Mermaid project")
|
||||
|
||||
|
||||
@project.command("save")
|
||||
@click.argument("path", required=False)
|
||||
def project_save(path: str | None) -> None:
|
||||
emit(project_mod.save_project(get_session(), path), "Saved Mermaid project")
|
||||
|
||||
|
||||
@project.command("info")
|
||||
def project_info() -> None:
|
||||
emit(project_mod.project_info(get_session()), "Project info")
|
||||
|
||||
|
||||
@project.command("samples")
|
||||
def project_samples() -> None:
|
||||
emit(project_mod.list_samples(), "Available samples")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def diagram() -> None:
|
||||
"""Diagram source commands."""
|
||||
|
||||
|
||||
@diagram.command("set")
|
||||
@click.option("--text", default=None, help="Inline Mermaid source")
|
||||
@click.option("--file", "file_path", default=None, help="Read Mermaid source from file")
|
||||
def diagram_set(text: str | None, file_path: str | None) -> None:
|
||||
session = get_session()
|
||||
if not session.is_open:
|
||||
raise click.ClickException("No project is open")
|
||||
if bool(text) == bool(file_path):
|
||||
raise click.ClickException("Provide exactly one of --text or --file")
|
||||
if file_path:
|
||||
with open(file_path, "r", encoding="utf-8") as fh:
|
||||
text = fh.read()
|
||||
emit(diagram_mod.set_diagram(session, text or ""), "Updated Mermaid source")
|
||||
|
||||
|
||||
@diagram.command("show")
|
||||
def diagram_show() -> None:
|
||||
result = diagram_mod.show_diagram(get_session())
|
||||
if _json_output:
|
||||
emit(result)
|
||||
else:
|
||||
click.echo(result["code"])
|
||||
|
||||
|
||||
@cli.group()
|
||||
def export() -> None:
|
||||
"""Render and share commands."""
|
||||
|
||||
|
||||
@export.command("render")
|
||||
@click.argument("output_path")
|
||||
@click.option("--format", "-f", "fmt", type=click.Choice(["svg", "png"]), default="svg")
|
||||
@click.option("--overwrite", is_flag=True, help="Overwrite existing output")
|
||||
def export_render(output_path: str, fmt: str, overwrite: bool) -> None:
|
||||
emit(export_mod.render(get_session(), output_path, fmt=fmt, overwrite=overwrite), "Rendered output")
|
||||
|
||||
|
||||
@export.command("share")
|
||||
@click.option("--mode", type=click.Choice(["edit", "view"]), default="edit")
|
||||
def export_share(mode: str) -> None:
|
||||
emit(export_mod.share(get_session(), mode=mode), "Generated share URL")
|
||||
|
||||
|
||||
@cli.group()
|
||||
def session() -> None:
|
||||
"""Session state commands."""
|
||||
|
||||
|
||||
@session.command("status")
|
||||
def session_status() -> None:
|
||||
emit(get_session().status(), "Session status")
|
||||
|
||||
|
||||
@session.command("undo")
|
||||
def session_undo() -> None:
|
||||
success = get_session().undo()
|
||||
emit({"action": "undo", "success": success}, "Undo complete" if success else "Nothing to undo")
|
||||
|
||||
|
||||
@session.command("redo")
|
||||
def session_redo() -> None:
|
||||
success = get_session().redo()
|
||||
emit({"action": "redo", "success": success}, "Redo complete" if success else "Nothing to redo")
|
||||
|
||||
|
||||
REPL_COMMANDS = {
|
||||
"new [sample]": "Create a Mermaid project",
|
||||
"open <path>": "Open a Mermaid project file",
|
||||
"save [path]": "Save the current project",
|
||||
"show": "Print the current Mermaid source",
|
||||
"set <text>": "Replace the Mermaid source text",
|
||||
"render <path> [svg|png]": "Render an artifact through the Mermaid renderer",
|
||||
"share [edit|view]": "Generate a Mermaid Live URL",
|
||||
"status": "Show session status",
|
||||
"undo": "Undo the last source change",
|
||||
"redo": "Redo the last undone change",
|
||||
"quit": "Exit the REPL",
|
||||
}
|
||||
|
||||
|
||||
@cli.command()
|
||||
def repl() -> None:
|
||||
"""Interactive REPL."""
|
||||
session = get_session()
|
||||
skin = ReplSkin("mermaid", version="1.0.0")
|
||||
skin.print_banner()
|
||||
prompt_session = skin.create_prompt_session()
|
||||
while True:
|
||||
project_name = session.project_path or "(unsaved)" if session.is_open else ""
|
||||
line = skin.get_input(prompt_session, project_name=project_name, modified=session.modified).strip()
|
||||
if not line:
|
||||
continue
|
||||
if line in {"quit", "exit"}:
|
||||
skin.print_goodbye()
|
||||
break
|
||||
if line == "help":
|
||||
skin.help(REPL_COMMANDS)
|
||||
continue
|
||||
try:
|
||||
parts = line.split()
|
||||
command, args = parts[0], parts[1:]
|
||||
if command == "new":
|
||||
sample = args[0] if args else "flowchart"
|
||||
emit(project_mod.new_project(session, sample=sample), "Created Mermaid project")
|
||||
elif command == "open":
|
||||
emit(project_mod.open_project(session, args[0]), "Opened Mermaid project")
|
||||
elif command == "save":
|
||||
path = args[0] if args else None
|
||||
emit(project_mod.save_project(session, path), "Saved Mermaid project")
|
||||
elif command == "show":
|
||||
click.echo(diagram_mod.show_diagram(session)["code"])
|
||||
elif command == "set":
|
||||
emit(diagram_mod.set_diagram(session, " ".join(args)), "Updated Mermaid source")
|
||||
elif command == "render":
|
||||
fmt = args[1] if len(args) > 1 else "svg"
|
||||
emit(export_mod.render(session, args[0], fmt=fmt, overwrite=True), "Rendered output")
|
||||
elif command == "share":
|
||||
mode = args[0] if args else "edit"
|
||||
emit(export_mod.share(session, mode=mode), "Generated share URL")
|
||||
elif command == "status":
|
||||
emit(session.status(), "Session status")
|
||||
elif command == "undo":
|
||||
emit({"action": "undo", "success": session.undo()})
|
||||
elif command == "redo":
|
||||
emit({"action": "redo", "success": session.redo()})
|
||||
else:
|
||||
skin.error(f"Unknown command: {command}")
|
||||
except Exception as exc: # pragma: no cover
|
||||
skin.error(str(exc))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
93
mermaid/agent-harness/cli_anything/mermaid/tests/TEST.md
Normal file
93
mermaid/agent-harness/cli_anything/mermaid/tests/TEST.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Mermaid CLI - Test Plan & Results
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Test Inventory Plan
|
||||
|
||||
- `test_core.py`: 5 unit tests planned
|
||||
- `test_full_e2e.py`: 5 E2E tests planned
|
||||
|
||||
### Unit Test Plan
|
||||
|
||||
**Session and project state**
|
||||
- default state generation
|
||||
- project save/open roundtrip
|
||||
- undo/redo state transitions
|
||||
- expected count: 3
|
||||
|
||||
**Backend serialization and URLs**
|
||||
- serialized `pako:` state generation
|
||||
- Mermaid renderer URL generation
|
||||
- Mermaid live edit/view URL generation
|
||||
- expected count: 2
|
||||
|
||||
**Share command behavior**
|
||||
- edit/view share payload generation
|
||||
- expected count: 1
|
||||
|
||||
### E2E Test Plan
|
||||
|
||||
**Installed CLI**
|
||||
- `--help` output from the installed command
|
||||
- JSON project creation through subprocess
|
||||
|
||||
**Real artifact generation**
|
||||
- render SVG through the Mermaid renderer service
|
||||
- render PNG through the Mermaid renderer service
|
||||
- verify SVG content and PNG magic bytes
|
||||
|
||||
**Workflow validation**
|
||||
- create a project
|
||||
- replace diagram text
|
||||
- auto-save through `--project`
|
||||
- generate a live-editor URL
|
||||
|
||||
### Realistic Workflow Scenarios
|
||||
|
||||
**Workflow name**: Mermaid smoke path
|
||||
**Simulates**: an agent creating and sharing a small flowchart
|
||||
**Operations chained**: create project, set code, render SVG, render PNG, generate view URL
|
||||
**Verified**: saved project file, renderer response, SVG markup, PNG magic bytes, live URL format
|
||||
|
||||
## Test Results
|
||||
|
||||
Command run:
|
||||
|
||||
```bash
|
||||
python -m pytest cli_anything/mermaid/tests/ -v --tb=no
|
||||
```
|
||||
|
||||
Latest result:
|
||||
|
||||
```text
|
||||
============================= test session starts =============================
|
||||
platform win32 -- Python 3.12.10, pytest-9.0.2, pluggy-1.6.0 -- C:\Users\gram\AppData\Local\Programs\Python\Python312\python.exe
|
||||
cachedir: .pytest_cache
|
||||
rootdir: C:\Users\gram\Downloads\코덱스 프로젝트\오픈소스 CLI변환\CLI-Anything\mermaid\agent-harness
|
||||
plugins: cov-7.0.0
|
||||
collecting ... collected 10 items
|
||||
|
||||
cli_anything/mermaid/tests/test_core.py::test_default_state PASSED
|
||||
cli_anything/mermaid/tests/test_core.py::test_save_open_roundtrip PASSED
|
||||
cli_anything/mermaid/tests/test_core.py::test_undo_redo PASSED
|
||||
cli_anything/mermaid/tests/test_core.py::test_backend_serialization_and_urls PASSED
|
||||
cli_anything/mermaid/tests/test_core.py::test_share_payload PASSED
|
||||
cli_anything/mermaid/tests/test_full_e2e.py::TestMermaidCLI::test_help PASSED
|
||||
cli_anything/mermaid/tests/test_full_e2e.py::TestMermaidCLI::test_project_new_json PASSED
|
||||
cli_anything/mermaid/tests/test_full_e2e.py::TestMermaidCLI::test_render_svg PASSED
|
||||
cli_anything/mermaid/tests/test_full_e2e.py::TestMermaidCLI::test_render_png PASSED
|
||||
cli_anything/mermaid/tests/test_full_e2e.py::TestMermaidCLI::test_share_view_url PASSED
|
||||
|
||||
============================= 10 passed in 10.60s =============================
|
||||
```
|
||||
|
||||
Summary:
|
||||
|
||||
- Total tests: 10
|
||||
- Pass rate: 100%
|
||||
- Execution time: 10.60s
|
||||
|
||||
Coverage notes:
|
||||
|
||||
- The harness verifies real SVG and PNG artifacts from the Mermaid renderer service.
|
||||
- The harness does not currently cover gist import, browser-only editor options, or external Mermaid Chart save flows.
|
||||
@@ -0,0 +1,55 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from cli_anything.mermaid.core import export as export_mod
|
||||
from cli_anything.mermaid.core import project as project_mod
|
||||
from cli_anything.mermaid.core.session import Session, default_state
|
||||
from cli_anything.mermaid.utils import mermaid_backend
|
||||
|
||||
|
||||
def test_default_state():
|
||||
state = default_state()
|
||||
assert "flowchart TD" in state["code"]
|
||||
assert json.loads(state["mermaid"])["theme"] == "default"
|
||||
|
||||
|
||||
def test_save_open_roundtrip():
|
||||
session = Session()
|
||||
project_mod.new_project(session, sample="sequence")
|
||||
with tempfile.NamedTemporaryFile(suffix=".mermaid.json", delete=False) as fh:
|
||||
path = fh.name
|
||||
try:
|
||||
project_mod.save_project(session, path)
|
||||
other = Session()
|
||||
result = project_mod.open_project(other, path)
|
||||
assert result["line_count"] > 0
|
||||
assert "sequenceDiagram" in other.state["code"]
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def test_undo_redo():
|
||||
session = Session()
|
||||
session.new_project()
|
||||
original = session.state["code"]
|
||||
session.set_code("graph TD\n A --> B\n")
|
||||
assert session.undo() is True
|
||||
assert session.state["code"] == original
|
||||
assert session.redo() is True
|
||||
assert "A --> B" in session.state["code"]
|
||||
|
||||
|
||||
def test_backend_serialization_and_urls():
|
||||
serialized = mermaid_backend.serialize_state(default_state())
|
||||
assert serialized.startswith("pako:")
|
||||
assert mermaid_backend.build_render_url(serialized, "svg").startswith("https://")
|
||||
assert mermaid_backend.build_live_url(serialized, "view").startswith("https://")
|
||||
|
||||
|
||||
def test_share_payload():
|
||||
session = Session()
|
||||
session.new_project()
|
||||
result = export_mod.share(session, mode="edit")
|
||||
assert result["mode"] == "edit"
|
||||
assert result["url"].startswith("https://mermaid.live/edit#")
|
||||
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _resolve_cli(name: str):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return [path]
|
||||
return [sys.executable, "-m", "cli_anything.mermaid"]
|
||||
|
||||
|
||||
class TestMermaidCLI:
|
||||
CLI = _resolve_cli("cli-anything-mermaid")
|
||||
|
||||
def _run(self, args):
|
||||
return subprocess.run(self.CLI + args, capture_output=True, text=True, check=True, timeout=60)
|
||||
|
||||
def test_help(self):
|
||||
result = self._run(["--help"])
|
||||
assert "Mermaid" in result.stdout
|
||||
|
||||
def test_project_new_json(self, tmp_path):
|
||||
path = str(tmp_path / "demo.mermaid.json")
|
||||
result = self._run(["--json", "project", "new", "-o", path])
|
||||
data = json.loads(result.stdout)
|
||||
assert data["action"] == "new_project"
|
||||
assert os.path.exists(path)
|
||||
|
||||
def test_render_svg(self, tmp_path):
|
||||
project_path = str(tmp_path / "demo.mermaid.json")
|
||||
output_path = str(tmp_path / "demo.svg")
|
||||
self._run(["project", "new", "-o", project_path])
|
||||
self._run(["--project", project_path, "diagram", "set", "--text", "graph TD; A[Test] --> B[Works]"])
|
||||
result = self._run(
|
||||
["--json", "--project", project_path, "export", "render", output_path, "-f", "svg", "--overwrite"]
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
assert data["action"] == "render"
|
||||
assert os.path.exists(output_path)
|
||||
with open(output_path, "r", encoding="utf-8") as fh:
|
||||
assert "<svg" in fh.read(200)
|
||||
|
||||
def test_render_png(self, tmp_path):
|
||||
project_path = str(tmp_path / "demo.mermaid.json")
|
||||
output_path = str(tmp_path / "demo.png")
|
||||
self._run(["project", "new", "-o", project_path])
|
||||
self._run(["--project", project_path, "diagram", "set", "--text", "graph TD; A[Test] --> B[Works]"])
|
||||
result = self._run(
|
||||
["--json", "--project", project_path, "export", "render", output_path, "-f", "png", "--overwrite"]
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
assert data["format"] == "png"
|
||||
with open(output_path, "rb") as fh:
|
||||
assert fh.read(4) == b"\x89PNG"
|
||||
|
||||
def test_share_view_url(self, tmp_path):
|
||||
project_path = str(tmp_path / "demo.mermaid.json")
|
||||
self._run(["project", "new", "-o", project_path])
|
||||
result = self._run(["--json", "--project", project_path, "export", "share", "--mode", "view"])
|
||||
data = json.loads(result.stdout)
|
||||
assert data["url"].startswith("https://mermaid.live/view#")
|
||||
@@ -0,0 +1,2 @@
|
||||
"""Mermaid utility modules."""
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Backend helpers for Mermaid Live Editor render/share state."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
import zlib
|
||||
|
||||
|
||||
RENDER_BASE = os.environ.get("MERMAID_RENDERER_URL", "https://mermaid.ink").rstrip("/")
|
||||
LIVE_BASE = os.environ.get("MERMAID_LIVE_URL", "https://mermaid.live").rstrip("/")
|
||||
|
||||
|
||||
def serialize_state(state: dict) -> str:
|
||||
payload = json.dumps(state, separators=(",", ":")).encode("utf-8")
|
||||
compressed = zlib.compress(payload, level=9)
|
||||
encoded = base64.urlsafe_b64encode(compressed).decode("ascii").rstrip("=")
|
||||
return f"pako:{encoded}"
|
||||
|
||||
|
||||
def build_render_url(serialized: str, fmt: str) -> str:
|
||||
if fmt == "svg":
|
||||
return f"{RENDER_BASE}/svg/{serialized}"
|
||||
if fmt == "png":
|
||||
return f"{RENDER_BASE}/img/{serialized}?type=png"
|
||||
raise ValueError(f"Unsupported format: {fmt}")
|
||||
|
||||
|
||||
def build_live_url(serialized: str, mode: str) -> str:
|
||||
if mode not in {"edit", "view"}:
|
||||
raise ValueError(f"Unsupported share mode: {mode}")
|
||||
return f"{LIVE_BASE}/{mode}#{serialized}"
|
||||
|
||||
|
||||
def render_to_file(serialized: str, output_path: str, fmt: str) -> dict:
|
||||
url = build_render_url(serialized, fmt)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=60) as response:
|
||||
data = response.read()
|
||||
parent = os.path.dirname(os.path.abspath(output_path))
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(output_path, "wb") as fh:
|
||||
fh.write(data)
|
||||
return {
|
||||
"output": os.path.abspath(output_path),
|
||||
"format": fmt,
|
||||
"method": "mermaid-renderer",
|
||||
"file_size": os.path.getsize(output_path),
|
||||
"url": url,
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Minimal REPL skin compatible with CLI-Anything REPL usage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.history import InMemoryHistory
|
||||
|
||||
|
||||
class ReplSkin:
|
||||
def __init__(self, software: str, version: str = "1.0.0"):
|
||||
self.software = software
|
||||
self.version = version
|
||||
|
||||
def print_banner(self) -> None:
|
||||
print(f"cli-anything-{self.software} v{self.version}")
|
||||
print("Type help for commands, quit to exit")
|
||||
|
||||
def create_prompt_session(self) -> PromptSession:
|
||||
return PromptSession(history=InMemoryHistory(), auto_suggest=AutoSuggestFromHistory())
|
||||
|
||||
def get_input(self, session: PromptSession, project_name: str = "", modified: bool = False) -> str:
|
||||
suffix = "*" if modified else ""
|
||||
ctx = f"[{project_name}{suffix}]" if project_name else ""
|
||||
return session.prompt(f"{self.software}{ctx}> ")
|
||||
|
||||
def help(self, commands: dict[str, str]) -> None:
|
||||
for command, desc in commands.items():
|
||||
print(f"{command}: {desc}")
|
||||
|
||||
def success(self, message: str) -> None:
|
||||
print(f"OK {message}")
|
||||
|
||||
def error(self, message: str) -> None:
|
||||
print(f"ERROR {message}")
|
||||
|
||||
def warning(self, message: str) -> None:
|
||||
print(f"WARN {message}")
|
||||
|
||||
def info(self, message: str) -> None:
|
||||
print(f"INFO {message}")
|
||||
|
||||
def print_goodbye(self) -> None:
|
||||
print("Goodbye!")
|
||||
29
mermaid/agent-harness/setup.py
Normal file
29
mermaid/agent-harness/setup.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from setuptools import find_namespace_packages, setup
|
||||
|
||||
with open("cli_anything/mermaid/README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name="cli-anything-mermaid",
|
||||
version="1.0.0",
|
||||
description="CLI harness for Mermaid Live Editor state files and renderer URLs",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/HKUDS/CLI-Anything",
|
||||
packages=find_namespace_packages(include=["cli_anything.*"]),
|
||||
install_requires=[
|
||||
"click>=8.0.0",
|
||||
"prompt-toolkit>=3.0.0",
|
||||
],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"pytest>=9.0.0",
|
||||
]
|
||||
},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"cli-anything-mermaid=cli_anything.mermaid.mermaid_cli:main",
|
||||
]
|
||||
},
|
||||
python_requires=">=3.10",
|
||||
)
|
||||
Reference in New Issue
Block a user