feat: Add cleanup command and improve artifact editor integration
Features: - Add `discussions cleanup` command to find orphaned diagrams - Scans all discussions for DIAGRAM: references - Lists unreferenced files in diagrams/ folders with sizes - --delete flag removes orphans with confirmation prompt - New Discussion dialog now shows and allows changing save location - Add Browse button to select output directory for new discussions Fixes: - Fix silent failure when artifact-editor not installed (now shows error) - Fix silent failure when artifact-editor missing GUI deps (PyQt6) - Add error logging for subprocess failures in artifact editor Dependencies: - smarttools now installed automatically from git URL - artifact-editor included in [gui] extra - PyQt6/QScintilla included for artifact-editor GUI support Also: - Add diagrams/ to .gitignore (generated test artifacts) - Update README with simplified installation instructions - Document cleanup command in README and CLAUDE.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c592b93597
commit
a39253c893
|
|
@ -49,3 +49,6 @@ htmlcov/
|
|||
|
||||
# Discussion files created during testing
|
||||
test_*.md
|
||||
|
||||
# Generated diagrams (keep examples/diagrams/ for documentation)
|
||||
diagrams/
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ pytest --cov=discussions
|
|||
# Test SmartTools directly (Unix philosophy - test tools independently)
|
||||
cat examples/brainstorm_notification_system.md | discussion-parser | jq .
|
||||
cat examples/brainstorm_notification_system.md | discussion-parser | discussion-vote-counter
|
||||
|
||||
# Find orphaned diagrams (not referenced by any discussion)
|
||||
discussions cleanup
|
||||
|
||||
# Delete orphaned diagrams with confirmation
|
||||
discussions cleanup --delete
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
|
|
|||
33
README.md
33
README.md
|
|
@ -41,12 +41,32 @@ discussions ui --tui
|
|||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone and install
|
||||
# Clone and install (SmartTools installed automatically from git)
|
||||
git clone https://gitea.brrd.tech/rob/orchestrated-discussions.git
|
||||
cd orchestrated-discussions
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# For TUI support only
|
||||
# With GUI support (includes artifact-editor for diagram creation)
|
||||
pip install -e ".[gui]"
|
||||
```
|
||||
|
||||
### From local checkouts (for development)
|
||||
|
||||
```bash
|
||||
# Clone all projects
|
||||
git clone https://gitea.brrd.tech/rob/SmartTools.git ~/PycharmProjects/SmartTools
|
||||
git clone https://gitea.brrd.tech/rob/artifact-editor.git ~/PycharmProjects/artifact-editor
|
||||
git clone https://gitea.brrd.tech/rob/orchestrated-discussions.git ~/PycharmProjects/orchestrated-discussions
|
||||
|
||||
# Install in order (editable mode)
|
||||
pip install -e ~/PycharmProjects/SmartTools
|
||||
pip install -e ~/PycharmProjects/artifact-editor
|
||||
pip install -e "~/PycharmProjects/orchestrated-discussions[dev]"
|
||||
```
|
||||
|
||||
### Minimal install (TUI only, no artifact editor)
|
||||
|
||||
```bash
|
||||
pip install -e ".[tui]"
|
||||
```
|
||||
|
||||
|
|
@ -77,7 +97,7 @@ docker-compose run --rm shell
|
|||
|
||||
- Python 3.10+
|
||||
- [SmartTools](https://gitea.brrd.tech/rob/SmartTools) (installed automatically)
|
||||
- At least one AI CLI tool (Claude, Codex, etc.)
|
||||
- At least one AI provider configured in SmartTools (Claude, Codex, Gemini, etc.)
|
||||
|
||||
## How It Works
|
||||
|
||||
|
|
@ -123,6 +143,7 @@ User/CLI/UI
|
|||
| `discussions comment <file> <text>` | Add a human comment |
|
||||
| `discussions participants` | List available participant SmartTools |
|
||||
| `discussions advance <file> --phase <id>` | Advance to a specific phase |
|
||||
| `discussions cleanup [directory]` | Find orphaned diagrams not referenced by any discussion |
|
||||
| `discussions ui [directory]` | Launch interactive UI |
|
||||
|
||||
### Examples
|
||||
|
|
@ -145,6 +166,12 @@ discussions comment my-feature.md "I agree with the concerns" --vote changes
|
|||
|
||||
# Check consensus status
|
||||
discussions status my-feature.md
|
||||
|
||||
# Find orphaned diagrams (not referenced by any discussion)
|
||||
discussions cleanup
|
||||
|
||||
# Delete orphaned diagrams (with confirmation)
|
||||
discussions cleanup --delete
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
|
|
|||
|
|
@ -28,17 +28,22 @@ classifiers = [
|
|||
]
|
||||
dependencies = [
|
||||
"PyYAML>=6.0",
|
||||
"smarttools>=0.1.0",
|
||||
"smarttools @ git+https://gitea.brrd.tech/rob/SmartTools.git",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
tui = [
|
||||
"urwid>=2.1.0",
|
||||
]
|
||||
gui = [
|
||||
"dearpygui>=2.0.0",
|
||||
"artifact-editor @ git+https://gitea.brrd.tech/rob/artifact-editor.git",
|
||||
]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"pytest-cov>=4.0",
|
||||
"urwid>=2.1.0",
|
||||
"dearpygui>=2.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
|
|
|||
|
|
@ -244,6 +244,114 @@ def cmd_advance(args) -> int:
|
|||
return 0
|
||||
|
||||
|
||||
def cmd_cleanup(args) -> int:
|
||||
"""Find and optionally delete orphaned diagrams."""
|
||||
from .discussion import Discussion
|
||||
from .markers import extract_diagrams
|
||||
import re
|
||||
|
||||
directory = Path(args.directory) if args.directory else Path.cwd()
|
||||
|
||||
if not directory.exists():
|
||||
print(f"Error: Directory not found: {directory}")
|
||||
return 1
|
||||
|
||||
# Find all discussion files
|
||||
discussions = list(directory.glob("**/*.discussion.md"))
|
||||
|
||||
# Collect all referenced diagrams from all discussions
|
||||
referenced = set()
|
||||
for disc_path in discussions:
|
||||
try:
|
||||
d = Discussion.load(disc_path)
|
||||
for comment in d.comments:
|
||||
diagrams = extract_diagrams(comment.body)
|
||||
for diagram in diagrams:
|
||||
# Resolve relative to discussion file
|
||||
abs_path = (disc_path.parent / diagram.path).resolve()
|
||||
referenced.add(abs_path)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse {disc_path}: {e}")
|
||||
|
||||
# Find all diagram directories
|
||||
diagram_dirs = set()
|
||||
|
||||
# Check for diagrams folders alongside discussions
|
||||
for disc_path in discussions:
|
||||
diagrams_dir = disc_path.parent / "diagrams"
|
||||
if diagrams_dir.exists():
|
||||
diagram_dirs.add(diagrams_dir)
|
||||
|
||||
# Also check top-level diagrams folder
|
||||
top_diagrams = directory / "diagrams"
|
||||
if top_diagrams.exists():
|
||||
diagram_dirs.add(top_diagrams)
|
||||
|
||||
# Scan for any diagrams/ folders recursively (catches orphaned folders with no discussions)
|
||||
for diagrams_dir in directory.glob("**/diagrams"):
|
||||
if diagrams_dir.is_dir():
|
||||
diagram_dirs.add(diagrams_dir)
|
||||
|
||||
if not diagram_dirs:
|
||||
print("No diagram directories found")
|
||||
return 0
|
||||
|
||||
# Find all diagram files
|
||||
diagram_extensions = {'.puml', '.plantuml', '.mmd', '.mermaid', '.svg', '.png', '.scad'}
|
||||
all_diagrams = set()
|
||||
for diagrams_dir in diagram_dirs:
|
||||
for ext in diagram_extensions:
|
||||
for f in diagrams_dir.glob(f"*{ext}"):
|
||||
all_diagrams.add(f.resolve())
|
||||
|
||||
# Find orphans
|
||||
orphans = all_diagrams - referenced
|
||||
|
||||
if not orphans:
|
||||
print(f"No orphaned diagrams found ({len(all_diagrams)} diagrams, all referenced)")
|
||||
return 0
|
||||
|
||||
print(f"Found {len(orphans)} orphaned diagram(s) out of {len(all_diagrams)} total:\n")
|
||||
|
||||
# Group by directory for display
|
||||
by_dir = {}
|
||||
for orphan in sorted(orphans):
|
||||
parent = orphan.parent
|
||||
if parent not in by_dir:
|
||||
by_dir[parent] = []
|
||||
by_dir[parent].append(orphan)
|
||||
|
||||
for parent, files in sorted(by_dir.items()):
|
||||
try:
|
||||
rel_parent = parent.relative_to(directory)
|
||||
except ValueError:
|
||||
rel_parent = parent
|
||||
print(f" {rel_parent}/")
|
||||
for f in files:
|
||||
size = f.stat().st_size
|
||||
size_str = f"{size:,} bytes" if size < 1024 else f"{size/1024:.1f} KB"
|
||||
print(f" {f.name} ({size_str})")
|
||||
|
||||
if args.delete:
|
||||
print()
|
||||
confirm = input(f"Delete {len(orphans)} orphaned file(s)? [y/N] ")
|
||||
if confirm.lower() == 'y':
|
||||
deleted = 0
|
||||
for orphan in orphans:
|
||||
try:
|
||||
orphan.unlink()
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
print(f" Error deleting {orphan.name}: {e}")
|
||||
print(f"Deleted {deleted} file(s)")
|
||||
else:
|
||||
print("Cancelled")
|
||||
else:
|
||||
print(f"\nRun with --delete to remove these files")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_ui(args) -> int:
|
||||
"""Launch the interactive UI (GUI by default, TUI with --tui)."""
|
||||
# Determine if path is a file or directory
|
||||
|
|
@ -340,6 +448,12 @@ def main(argv: list[str] = None) -> int:
|
|||
p_advance.add_argument("--phase", help="Target phase ID")
|
||||
p_advance.set_defaults(func=cmd_advance)
|
||||
|
||||
# 'cleanup' command
|
||||
p_cleanup = subparsers.add_parser("cleanup", help="Find orphaned diagrams")
|
||||
p_cleanup.add_argument("directory", nargs="?", help="Directory to scan (default: current)")
|
||||
p_cleanup.add_argument("--delete", "-d", action="store_true", help="Delete orphaned files (with confirmation)")
|
||||
p_cleanup.set_defaults(func=cmd_cleanup)
|
||||
|
||||
# 'ui' command
|
||||
p_ui = subparsers.add_parser("ui", help="Launch interactive UI")
|
||||
p_ui.add_argument("path", nargs="?", help="Discussion file (.md) or directory to browse")
|
||||
|
|
|
|||
|
|
@ -534,6 +534,39 @@ class DiscussionGUI:
|
|||
self.discussions_dir = Path(folder_path)
|
||||
self._refresh_discussions()
|
||||
|
||||
def _browse_for_folder(self, target_input_tag: str):
|
||||
"""Show folder browser and update a target input field with selected path.
|
||||
|
||||
Args:
|
||||
target_input_tag: Tag of the input_text widget to update with selection
|
||||
"""
|
||||
# Create a one-time folder dialog for this specific purpose
|
||||
dialog_tag = f"browse_folder_{target_input_tag}"
|
||||
|
||||
def on_select(sender, app_data):
|
||||
folder_path = app_data.get("file_path_name", "")
|
||||
if folder_path and dpg.does_item_exist(target_input_tag):
|
||||
dpg.set_value(target_input_tag, folder_path)
|
||||
dpg.delete_item(dialog_tag)
|
||||
|
||||
def on_cancel(sender, app_data):
|
||||
dpg.delete_item(dialog_tag)
|
||||
|
||||
# Get current value to start from
|
||||
current = dpg.get_value(target_input_tag) if dpg.does_item_exist(target_input_tag) else str(Path.cwd())
|
||||
|
||||
with dpg.file_dialog(
|
||||
directory_selector=True,
|
||||
show=True,
|
||||
callback=on_select,
|
||||
cancel_callback=on_cancel,
|
||||
tag=dialog_tag,
|
||||
default_path=current,
|
||||
width=700,
|
||||
height=400
|
||||
):
|
||||
pass
|
||||
|
||||
def _refresh_discussions(self):
|
||||
"""Refresh the list of discussions."""
|
||||
self.discussions_list = []
|
||||
|
|
@ -869,6 +902,7 @@ class DiscussionGUI:
|
|||
"""
|
||||
cmd, env = self._get_artifact_editor_cmd()
|
||||
if not cmd:
|
||||
self._add_output("Error: Artifact editor not found")
|
||||
return None
|
||||
|
||||
cmd.extend(["--output", str(file_path)])
|
||||
|
|
@ -881,6 +915,14 @@ class DiscussionGUI:
|
|||
env=env,
|
||||
)
|
||||
|
||||
# Log any stderr output for debugging
|
||||
if result.stderr:
|
||||
self._add_output(f"Artifact editor: {result.stderr[:200]}")
|
||||
|
||||
# Check return code
|
||||
if result.returncode != 0:
|
||||
self._add_output(f"Artifact editor exited with code {result.returncode}")
|
||||
|
||||
# Parse ARTIFACT_SAVED:path from stdout
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith("ARTIFACT_SAVED:"):
|
||||
|
|
@ -888,6 +930,7 @@ class DiscussionGUI:
|
|||
|
||||
return None
|
||||
except Exception as e:
|
||||
self._add_output(f"Error launching artifact editor: {e}")
|
||||
return None
|
||||
|
||||
def _create_new_artifact(self):
|
||||
|
|
@ -900,6 +943,19 @@ class DiscussionGUI:
|
|||
self._show_error("No discussion loaded")
|
||||
return
|
||||
|
||||
# Check if artifact editor is available before proceeding
|
||||
cmd, env = self._get_artifact_editor_cmd()
|
||||
if not cmd:
|
||||
self._show_error(
|
||||
"Artifact Editor not installed.\n\n"
|
||||
"Install from ~/PycharmProjects/artifact-editor:\n"
|
||||
" cd ~/PycharmProjects/artifact-editor\n"
|
||||
" ./install.sh\n\n"
|
||||
"Or install with pip:\n"
|
||||
" pip install -e ~/PycharmProjects/artifact-editor"
|
||||
)
|
||||
return
|
||||
|
||||
# Create diagrams directory if needed
|
||||
diagrams_dir = self.current_discussion.path.parent / "diagrams"
|
||||
diagrams_dir.mkdir(exist_ok=True)
|
||||
|
|
@ -1712,6 +1768,20 @@ class DiscussionGUI:
|
|||
to the comment only after we know the actual saved path (user may change format).
|
||||
"""
|
||||
if not self.current_discussion:
|
||||
self._show_error("No discussion loaded. Open a discussion first.")
|
||||
return
|
||||
|
||||
# Check if artifact editor is available before proceeding
|
||||
cmd, env = self._get_artifact_editor_cmd()
|
||||
if not cmd:
|
||||
self._show_error(
|
||||
"Artifact Editor not installed.\n\n"
|
||||
"Install from ~/PycharmProjects/artifact-editor:\n"
|
||||
" cd ~/PycharmProjects/artifact-editor\n"
|
||||
" ./install.sh\n\n"
|
||||
"Or install with pip:\n"
|
||||
" pip install -e ~/PycharmProjects/artifact-editor"
|
||||
)
|
||||
return
|
||||
|
||||
# Create diagrams directory if needed
|
||||
|
|
@ -2046,8 +2116,14 @@ class DiscussionGUI:
|
|||
slug = title.lower().replace(" ", "-")
|
||||
slug = "".join(c for c in slug if c.isalnum() or c == "-")
|
||||
|
||||
# Determine output directory
|
||||
output_dir = self.discussions_dir if self.discussions_dir else Path.cwd()
|
||||
# Get output directory from the location field
|
||||
output_dir = Path(dpg.get_value("new_disc_location"))
|
||||
if not output_dir.exists():
|
||||
try:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
self._show_error(f"Cannot create directory: {e}")
|
||||
return
|
||||
output_path = output_dir / f"{slug}.discussion.md"
|
||||
|
||||
# Check if file exists
|
||||
|
|
@ -2076,12 +2152,29 @@ class DiscussionGUI:
|
|||
except Exception as e:
|
||||
self._show_error(f"Failed to create discussion: {e}")
|
||||
|
||||
# Determine initial output directory
|
||||
if self.current_discussion:
|
||||
initial_dir = self.current_discussion.path.parent
|
||||
else:
|
||||
initial_dir = self.discussions_dir if self.discussions_dir else Path.cwd()
|
||||
|
||||
with dpg.window(label="New Discussion", tag=window_tag,
|
||||
width=550, height=520, pos=[400, 150], no_collapse=True):
|
||||
width=550, height=550, pos=[400, 150], no_collapse=True):
|
||||
|
||||
dpg.add_text("Title:", color=(150, 200, 255))
|
||||
dpg.add_input_text(tag="new_disc_title", width=-1, hint="Enter discussion title")
|
||||
|
||||
dpg.add_spacer(height=5)
|
||||
dpg.add_text("Location:", color=(150, 200, 255))
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_input_text(
|
||||
tag="new_disc_location",
|
||||
default_value=str(initial_dir),
|
||||
width=-80,
|
||||
readonly=False
|
||||
)
|
||||
dpg.add_button(label="Browse", callback=lambda: self._browse_for_folder("new_disc_location"))
|
||||
|
||||
dpg.add_spacer(height=10)
|
||||
dpg.add_text("Template:", color=(150, 200, 255))
|
||||
template_items = templates + ["+ Create New Template..."]
|
||||
|
|
|
|||
Loading…
Reference in New Issue