generated from coulomb/repo-seed
chore: workplan MRKD-WP-0001 + improved CLAUDE.md; document next steps
- workplans/MRKD-WP-0001-foundation-level1.md: 8-task workplan for Foundation & LEVEL1 Core (T01 scaffolding through T08 e2e harness) - CLAUDE.md: added Planned Architecture section (interface table, FR domain map, key concepts, round-trip data flow) and Development Commands stubs derived from FRS v0.2 - problems/next-steps-2026-03-14.md: implementation guide for next session — task order, dep list, state-hub task UUIDs, quality gates No code implemented yet; workstream registered in State Hub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
src/markidocx/__init__.py
Normal file
3
src/markidocx/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""markidocx — Markdown ↔ DOCX round-trip editing system."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
261
src/markidocx/cli.py
Normal file
261
src/markidocx/cli.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""CLI entry point for markidocx."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Optional
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
app = typer.Typer(
|
||||
name="markidocx",
|
||||
help="Markdown ↔ DOCX round-trip editing system.",
|
||||
add_completion=False,
|
||||
)
|
||||
template_app = typer.Typer(help="Template family management.")
|
||||
app.add_typer(template_app, name="template")
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
@app.command()
|
||||
def validate(
|
||||
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
|
||||
) -> None:
|
||||
"""Validate a manifest file (FR-100)."""
|
||||
from markidocx.manifest import ManifestError, load_manifest
|
||||
|
||||
try:
|
||||
m = load_manifest(manifest)
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "ok", "project": m.project.name}))
|
||||
else:
|
||||
console.print(f"[green]✓[/green] Manifest valid: [bold]{m.project.name}[/bold]")
|
||||
raise typer.Exit(0)
|
||||
except ManifestError as exc:
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
|
||||
else:
|
||||
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def build(
|
||||
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
|
||||
) -> None:
|
||||
"""Build a DOCX from Markdown sources (FR-200)."""
|
||||
from markidocx.builder import build_document
|
||||
from markidocx.manifest import ManifestError, load_manifest
|
||||
|
||||
try:
|
||||
m = load_manifest(manifest)
|
||||
except ManifestError as exc:
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
|
||||
else:
|
||||
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
result = build_document(m)
|
||||
if json_output:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok" if result.success else "error",
|
||||
"output_path": str(result.output_path),
|
||||
"family": result.family,
|
||||
"feature_level": result.feature_level,
|
||||
"warnings": result.warnings,
|
||||
"errors": result.errors,
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
if result.success:
|
||||
console.print(f"[green]✓[/green] Built: [bold]{result.output_path}[/bold]")
|
||||
for w in result.warnings:
|
||||
console.print(f"[yellow]⚠[/yellow] {w}")
|
||||
else:
|
||||
for e in result.errors:
|
||||
err_console.print(f"[red]✗[/red] {e}")
|
||||
|
||||
raise typer.Exit(0 if result.success else 1)
|
||||
|
||||
|
||||
@app.command("import")
|
||||
def import_docx(
|
||||
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
|
||||
docx: Annotated[Path, typer.Argument(help="Path to DOCX file to import")],
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
|
||||
) -> None:
|
||||
"""Import a DOCX back to Markdown (FR-300)."""
|
||||
from markidocx.importer import import_document
|
||||
from markidocx.manifest import ManifestError, load_manifest
|
||||
|
||||
try:
|
||||
m = load_manifest(manifest)
|
||||
except ManifestError as exc:
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
|
||||
else:
|
||||
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
result = import_document(m, docx)
|
||||
if json_output:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "ok" if result.success else "error",
|
||||
"output_files": [str(p) for p in result.output_files],
|
||||
"mapping_status": result.mapping_status,
|
||||
"warnings": result.warnings,
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
if result.success:
|
||||
console.print(f"[green]✓[/green] Imported ({result.mapping_status})")
|
||||
for f in result.output_files:
|
||||
console.print(f" → {f}")
|
||||
for w in result.warnings:
|
||||
console.print(f"[yellow]⚠[/yellow] {w}")
|
||||
else:
|
||||
err_console.print("[red]✗ Import failed[/red]")
|
||||
|
||||
raise typer.Exit(0 if result.success else 1)
|
||||
|
||||
|
||||
@app.command()
|
||||
def compare(
|
||||
manifest: Annotated[Path, typer.Argument(help="Path to manifest YAML file")],
|
||||
docx: Annotated[Path, typer.Argument(help="Path to DOCX file to compare against")],
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
|
||||
) -> None:
|
||||
"""Compare original Markdown with re-imported DOCX (FR-700)."""
|
||||
from markidocx.builder import build_document
|
||||
from markidocx.differ import compare as do_compare
|
||||
from markidocx.importer import import_document
|
||||
from markidocx.manifest import ManifestError, load_manifest
|
||||
|
||||
try:
|
||||
m = load_manifest(manifest)
|
||||
except ManifestError as exc:
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
|
||||
else:
|
||||
err_console.print(f"[red]✗ Manifest error:[/red] {exc}")
|
||||
raise typer.Exit(2)
|
||||
|
||||
# Read original markdown
|
||||
original_parts: list[str] = []
|
||||
for src in m.sources:
|
||||
p = manifest.parent / src.path
|
||||
original_parts.append(p.read_text(encoding="utf-8"))
|
||||
original_md = "\n\n".join(original_parts)
|
||||
|
||||
# Import the docx
|
||||
result = import_document(m, docx)
|
||||
if not result.success:
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "error", "message": "Import failed"}))
|
||||
else:
|
||||
err_console.print("[red]✗ Import failed — cannot compare[/red]")
|
||||
raise typer.Exit(2)
|
||||
|
||||
reimported_parts: list[str] = []
|
||||
for f in result.output_files:
|
||||
reimported_parts.append(Path(f).read_text(encoding="utf-8"))
|
||||
reimported_md = "\n\n".join(reimported_parts)
|
||||
|
||||
report = do_compare(original_md, reimported_md)
|
||||
|
||||
if json_output:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
{
|
||||
"has_drift": report.has_drift,
|
||||
"preserved": report.preserved,
|
||||
"degraded": report.degraded,
|
||||
"broken": report.broken,
|
||||
"unsupported": report.unsupported,
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
if report.has_drift:
|
||||
console.print("[yellow]⚠ Drift detected[/yellow]")
|
||||
for item in report.degraded:
|
||||
console.print(f" [yellow]degraded:[/yellow] {item}")
|
||||
for item in report.broken:
|
||||
console.print(f" [red]broken:[/red] {item}")
|
||||
else:
|
||||
console.print("[green]✓[/green] No drift detected")
|
||||
if report.preserved:
|
||||
console.print(f" preserved: {len(report.preserved)} elements")
|
||||
|
||||
raise typer.Exit(1 if report.has_drift else 0)
|
||||
|
||||
|
||||
@template_app.command("list")
|
||||
def template_list(
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
|
||||
) -> None:
|
||||
"""List available template families (FR-603)."""
|
||||
from markidocx.templates import FamilyRegistry
|
||||
|
||||
registry = FamilyRegistry()
|
||||
families = registry.list_families()
|
||||
|
||||
if json_output:
|
||||
typer.echo(
|
||||
json.dumps(
|
||||
[{"name": f.name, "description": f.description} for f in families]
|
||||
)
|
||||
)
|
||||
else:
|
||||
table = Table(title="Template Families")
|
||||
table.add_column("Name", style="bold")
|
||||
table.add_column("Description")
|
||||
for f in families:
|
||||
table.add_row(f.name, f.description)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@template_app.command("register")
|
||||
def template_register(
|
||||
path: Annotated[Path, typer.Argument(help="Path to .docx template")],
|
||||
name: Annotated[str, typer.Option("--name", help="Family name")] = "",
|
||||
description: Annotated[str, typer.Option("--description", help="Family description")] = "",
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Machine-readable JSON output")] = False,
|
||||
) -> None:
|
||||
"""Register a custom template family (FR-605)."""
|
||||
from markidocx.templates import FamilyRegistry
|
||||
|
||||
registry = FamilyRegistry()
|
||||
if not name:
|
||||
name = path.stem
|
||||
try:
|
||||
registry.register(path, name, description)
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "ok", "name": name}))
|
||||
else:
|
||||
console.print(f"[green]✓[/green] Registered template: [bold]{name}[/bold]")
|
||||
except Exception as exc:
|
||||
if json_output:
|
||||
typer.echo(json.dumps({"status": "error", "message": str(exc)}))
|
||||
else:
|
||||
err_console.print(f"[red]✗[/red] {exc}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
Reference in New Issue
Block a user