Add ExportOptions and auto-branch push to Scrivener export

This commit is contained in:
Solaria
2026-03-14 12:52:32 +00:00
parent 0ebf662d59
commit c42556d147
2 changed files with 161 additions and 3 deletions
+13
View File
@@ -103,6 +103,17 @@ def __getattr__(name: str):
from opus_orchestrator.utils.retry import with_retry
return with_retry
# Scrivener Export
if name == "ScrivenerExporter":
from opus_orchestrator.scrivener_export import ScrivenerExporter
return ScrivenerExporter
if name == "export_to_scrivener":
from opus_orchestrator.scrivener_export import export_to_scrivener
return export_to_scrivener
if name == "ExportOptions":
from opus_orchestrator.scrivener_export import ExportOptions
return ExportOptions
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -145,4 +156,6 @@ __all__ = [
# Scrivener Export
"ScrivenerExporter",
"export_to_scrivener",
"ExportOptions",
"ExportOptions",
]
+148 -3
View File
@@ -1,17 +1,36 @@
"""Scrivener-style output for Opus Orchestrator.
Generates chapter-by-chapter output with binder.json metadata.
Supports auto-branching and push to GitHub.
"""
import json
import re
import subprocess
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, asdict
from datetime import datetime
from opus_orchestrator.schemas import Manuscript, Chapter
@dataclass
class ExportOptions:
"""Options for Scrivener export."""
output_dir: str = "./output"
split_chapters: bool = True
branch: str = "draft/generated"
push_to_remote: bool = False
commit_message: str = ""
author_name: str = "Opus Orchestrator"
author_email: str = "opus@clowder.net"
def __post_init__(self):
if not self.commit_message:
self.commit_message = f"Auto-export: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
@dataclass
class ChapterFile:
"""A single chapter file."""
@@ -50,22 +69,47 @@ class ScrivenerExporter:
self,
manuscript: Manuscript,
book_title: str,
split_chapters: bool = True,
options: Optional[ExportOptions] = None,
) -> dict:
"""Export manuscript to Scrivener-style structure.
Args:
manuscript: The Manuscript to export
book_title: Title for the book folder
split_chapters: If True, split into individual files
options: ExportOptions (optional)
Returns:
Export metadata with file paths and word counts
"""
opts = options or ExportOptions(
output_dir=opts.output_dir if options else "./output",
split_chapters=options.split_chapters if options else True,
)
# Create book folder
book_folder = self.output_dir / self._slugify(book_title)
book_folder.mkdir(parents=True, exist_ok=True)
if opts.split_chapters:
result = self._export_split(manuscript, book_folder, book_title)
else:
result = self._export_single(manuscript, book_folder, book_title)
# Optionally push to GitHub
if opts.push_to_remote:
push_result = self._push_to_git(
book_folder,
opts.branch,
opts.commit_message,
opts.author_name,
opts.author_email,
)
result.update(push_result)
return result
book_folder = self.output_dir / self._slugify(book_title)
book_folder.mkdir(parents=True, exist_ok=True)
if split_chapters:
return self._export_split(manuscript, book_folder, book_title)
else:
@@ -232,11 +276,101 @@ word_count: {word_count}
return slug.strip('-')
def _push_to_git(
self,
folder: Path,
branch: str,
commit_message: str,
author_name: str,
author_email: str,
) -> dict:
"""Push exported folder to GitHub.
Args:
folder: Folder to push
branch: Branch name (will be created if doesn't exist)
commit_message: Commit message
author_name: Git author name
author_email: Git author email
Returns:
Push result metadata
"""
import subprocess
# Check if git repo exists
git_dir = folder / ".git"
if not git_dir.exists():
# Initialize new repo
subprocess.run(["git", "init"], cwd=folder, check=True, capture_output=True)
subprocess.run(["git", "config", "user.email", author_email], cwd=folder, check=True, capture_output=True)
subprocess.run(["git", "config", "user.name", author_name], cwd=folder, check=True, capture_output=True)
# Add all files
subprocess.run(["git", "add", "."], cwd=folder, check=True, capture_output=True)
# Check if there are changes
result = subprocess.run(
["git", "status", "--porcelain"],
cwd=folder,
capture_output=True,
text=True
)
if not result.stdout.strip():
return {"pushed": False, "reason": "No changes to commit"}
# Commit
subprocess.run(
["git", "commit", "-m", commit_message],
cwd=folder,
check=True,
capture_output=True
)
# Get or add remote
remotes = subprocess.run(
["git", "remote", "-v"],
cwd=folder,
capture_output=True,
text=True
)
if not remotes.stdout.strip():
# No remote set - can't push
return {
"pushed": False,
"reason": "No remote configured. Add one with: git remote add origin <url>"
}
# Push to branch
try:
subprocess.run(
["git", "push", "-u", "origin", branch, "--no-verify"],
cwd=folder,
check=True,
capture_output=True
)
return {
"pushed": True,
"branch": branch,
"commit_message": commit_message,
}
except subprocess.CalledProcessError as e:
return {
"pushed": False,
"reason": f"Push failed: {e.stderr.decode() if e.stderr else str(e)}"
}
def export_to_scrivener(
manuscript: Manuscript,
book_title: str,
output_dir: str = "./output",
split_chapters: bool = True,
branch: str = "draft/generated",
push_to_remote: bool = False,
commit_message: str = "",
) -> dict:
"""Convenience function to export in Scrivener style.
@@ -245,9 +379,20 @@ def export_to_scrivener(
book_title: Title for the book
output_dir: Output directory
split_chapters: Split into individual chapter files
branch: Branch to push to (default: draft/generated)
push_to_remote: Whether to push to GitHub remote
commit_message: Custom commit message
Returns:
Export metadata
"""
options = ExportOptions(
output_dir=output_dir,
split_chapters=split_chapters,
branch=branch,
push_to_remote=push_to_remote,
commit_message=commit_message,
)
exporter = ScrivenerExporter(output_dir)
return exporter.export(manuscript, book_title, split_chapters)
return exporter.export(manuscript, book_title, options)