doit-toolkit-cli 0.1.10__py3-none-any.whl
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.
Potentially problematic release.
This version of doit-toolkit-cli might be problematic. Click here for more details.
- doit_cli/__init__.py +1356 -0
- doit_cli/cli/__init__.py +26 -0
- doit_cli/cli/analytics_command.py +616 -0
- doit_cli/cli/context_command.py +213 -0
- doit_cli/cli/diagram_command.py +304 -0
- doit_cli/cli/fixit_command.py +641 -0
- doit_cli/cli/hooks_command.py +211 -0
- doit_cli/cli/init_command.py +613 -0
- doit_cli/cli/memory_command.py +293 -0
- doit_cli/cli/roadmapit_command.py +10 -0
- doit_cli/cli/status_command.py +117 -0
- doit_cli/cli/sync_prompts_command.py +248 -0
- doit_cli/cli/validate_command.py +196 -0
- doit_cli/cli/verify_command.py +204 -0
- doit_cli/cli/workflow_mixin.py +224 -0
- doit_cli/cli/xref_command.py +555 -0
- doit_cli/formatters/__init__.py +8 -0
- doit_cli/formatters/base.py +38 -0
- doit_cli/formatters/json_formatter.py +126 -0
- doit_cli/formatters/markdown_formatter.py +97 -0
- doit_cli/formatters/rich_formatter.py +257 -0
- doit_cli/main.py +51 -0
- doit_cli/models/__init__.py +139 -0
- doit_cli/models/agent.py +74 -0
- doit_cli/models/analytics_models.py +384 -0
- doit_cli/models/context_config.py +464 -0
- doit_cli/models/crossref_models.py +182 -0
- doit_cli/models/diagram_models.py +363 -0
- doit_cli/models/fixit_models.py +355 -0
- doit_cli/models/hook_config.py +125 -0
- doit_cli/models/project.py +91 -0
- doit_cli/models/results.py +121 -0
- doit_cli/models/search_models.py +228 -0
- doit_cli/models/status_models.py +195 -0
- doit_cli/models/sync_models.py +146 -0
- doit_cli/models/template.py +77 -0
- doit_cli/models/validation_models.py +175 -0
- doit_cli/models/workflow_models.py +319 -0
- doit_cli/prompts/__init__.py +5 -0
- doit_cli/prompts/fixit_prompts.py +344 -0
- doit_cli/prompts/interactive.py +390 -0
- doit_cli/rules/__init__.py +5 -0
- doit_cli/rules/builtin_rules.py +160 -0
- doit_cli/services/__init__.py +79 -0
- doit_cli/services/agent_detector.py +168 -0
- doit_cli/services/analytics_service.py +218 -0
- doit_cli/services/architecture_generator.py +290 -0
- doit_cli/services/backup_service.py +204 -0
- doit_cli/services/config_loader.py +113 -0
- doit_cli/services/context_loader.py +1123 -0
- doit_cli/services/coverage_calculator.py +142 -0
- doit_cli/services/crossref_service.py +237 -0
- doit_cli/services/cycle_time_calculator.py +134 -0
- doit_cli/services/date_inferrer.py +349 -0
- doit_cli/services/diagram_service.py +337 -0
- doit_cli/services/drift_detector.py +109 -0
- doit_cli/services/entity_parser.py +301 -0
- doit_cli/services/er_diagram_generator.py +197 -0
- doit_cli/services/fixit_service.py +699 -0
- doit_cli/services/github_service.py +192 -0
- doit_cli/services/hook_manager.py +258 -0
- doit_cli/services/hook_validator.py +528 -0
- doit_cli/services/input_validator.py +322 -0
- doit_cli/services/memory_search.py +527 -0
- doit_cli/services/mermaid_validator.py +334 -0
- doit_cli/services/prompt_transformer.py +91 -0
- doit_cli/services/prompt_writer.py +133 -0
- doit_cli/services/query_interpreter.py +428 -0
- doit_cli/services/report_exporter.py +219 -0
- doit_cli/services/report_generator.py +256 -0
- doit_cli/services/requirement_parser.py +112 -0
- doit_cli/services/roadmap_summarizer.py +209 -0
- doit_cli/services/rule_engine.py +443 -0
- doit_cli/services/scaffolder.py +215 -0
- doit_cli/services/score_calculator.py +172 -0
- doit_cli/services/section_parser.py +204 -0
- doit_cli/services/spec_scanner.py +327 -0
- doit_cli/services/state_manager.py +355 -0
- doit_cli/services/status_reporter.py +143 -0
- doit_cli/services/task_parser.py +347 -0
- doit_cli/services/template_manager.py +710 -0
- doit_cli/services/template_reader.py +158 -0
- doit_cli/services/user_journey_generator.py +214 -0
- doit_cli/services/user_story_parser.py +232 -0
- doit_cli/services/validation_service.py +188 -0
- doit_cli/services/validator.py +232 -0
- doit_cli/services/velocity_tracker.py +173 -0
- doit_cli/services/workflow_engine.py +405 -0
- doit_cli/templates/agent-file-template.md +28 -0
- doit_cli/templates/checklist-template.md +39 -0
- doit_cli/templates/commands/doit.checkin.md +363 -0
- doit_cli/templates/commands/doit.constitution.md +187 -0
- doit_cli/templates/commands/doit.documentit.md +485 -0
- doit_cli/templates/commands/doit.fixit.md +181 -0
- doit_cli/templates/commands/doit.implementit.md +265 -0
- doit_cli/templates/commands/doit.planit.md +262 -0
- doit_cli/templates/commands/doit.reviewit.md +355 -0
- doit_cli/templates/commands/doit.roadmapit.md +389 -0
- doit_cli/templates/commands/doit.scaffoldit.md +458 -0
- doit_cli/templates/commands/doit.specit.md +521 -0
- doit_cli/templates/commands/doit.taskit.md +304 -0
- doit_cli/templates/commands/doit.testit.md +277 -0
- doit_cli/templates/config/context.yaml +134 -0
- doit_cli/templates/config/hooks.yaml +93 -0
- doit_cli/templates/config/validation-rules.yaml +64 -0
- doit_cli/templates/github-issue-templates/epic.yml +78 -0
- doit_cli/templates/github-issue-templates/feature.yml +116 -0
- doit_cli/templates/github-issue-templates/task.yml +129 -0
- doit_cli/templates/hooks/.gitkeep +0 -0
- doit_cli/templates/hooks/post-commit.sh +25 -0
- doit_cli/templates/hooks/post-merge.sh +75 -0
- doit_cli/templates/hooks/pre-commit.sh +17 -0
- doit_cli/templates/hooks/pre-push.sh +18 -0
- doit_cli/templates/memory/completed_roadmap.md +50 -0
- doit_cli/templates/memory/constitution.md +125 -0
- doit_cli/templates/memory/roadmap.md +61 -0
- doit_cli/templates/plan-template.md +146 -0
- doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
- doit_cli/templates/scripts/bash/common.sh +156 -0
- doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
- doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
- doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
- doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
- doit_cli/templates/scripts/powershell/common.ps1 +137 -0
- doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
- doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
- doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
- doit_cli/templates/spec-template.md +159 -0
- doit_cli/templates/tasks-template.md +313 -0
- doit_cli/templates/vscode-settings.json +14 -0
- doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
- doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.10.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""CLI commands for AI context management."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from ..models.context_config import ContextConfig
|
|
10
|
+
from ..services.context_loader import ContextLoader
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
# Create the context command group
|
|
15
|
+
context_app = typer.Typer(
|
|
16
|
+
name="context",
|
|
17
|
+
help="Manage AI context injection for doit commands.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@context_app.command("show")
|
|
23
|
+
def show_context(
|
|
24
|
+
command: str = typer.Option(
|
|
25
|
+
None, "--command", "-c", help="Show context for specific command (applies overrides)"
|
|
26
|
+
),
|
|
27
|
+
verbose: bool = typer.Option(
|
|
28
|
+
False, "--verbose", "-v", help="Show full content of each source"
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Show what context would be loaded for commands."""
|
|
32
|
+
project_root = Path.cwd()
|
|
33
|
+
|
|
34
|
+
# Load configuration
|
|
35
|
+
config = ContextConfig.load_from_project(project_root)
|
|
36
|
+
|
|
37
|
+
console.print("[bold]AI Context Status[/bold]")
|
|
38
|
+
console.print("=" * 50)
|
|
39
|
+
|
|
40
|
+
# Show global status
|
|
41
|
+
if not config.enabled:
|
|
42
|
+
console.print("\n[yellow]Context loading is disabled globally.[/yellow]")
|
|
43
|
+
console.print("Enable in .doit/config/context.yaml by setting 'enabled: true'")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
console.print(f"\n[bold]Global Settings:[/bold]")
|
|
47
|
+
console.print(f" Enabled: [green]yes[/green]")
|
|
48
|
+
console.print(f" Max tokens per source: {config.max_tokens_per_source:,}")
|
|
49
|
+
console.print(f" Total max tokens: {config.total_max_tokens:,}")
|
|
50
|
+
|
|
51
|
+
# Show summarization settings
|
|
52
|
+
if config.summarization.enabled:
|
|
53
|
+
soft_threshold = int(config.total_max_tokens * (config.summarization.threshold_percentage / 100.0))
|
|
54
|
+
console.print(f"\n[bold]Summarization:[/bold]")
|
|
55
|
+
console.print(f" Enabled: [green]yes[/green]")
|
|
56
|
+
console.print(f" Soft threshold: {soft_threshold:,} tokens ({config.summarization.threshold_percentage}%)")
|
|
57
|
+
console.print(f" Fallback to truncation: {'yes' if config.summarization.fallback_to_truncation else 'no'}")
|
|
58
|
+
|
|
59
|
+
# Load context
|
|
60
|
+
loader = ContextLoader(
|
|
61
|
+
project_root=project_root,
|
|
62
|
+
config=config,
|
|
63
|
+
command=command,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
context = loader.load()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
console.print(f"\n[red]Error loading context: {e}[/red]")
|
|
70
|
+
raise typer.Exit(1)
|
|
71
|
+
|
|
72
|
+
# Show command-specific info
|
|
73
|
+
if command:
|
|
74
|
+
console.print(f"\n[bold]Context for command:[/bold] {command}")
|
|
75
|
+
if command in config.commands:
|
|
76
|
+
console.print(" [dim]Command-specific overrides applied[/dim]")
|
|
77
|
+
|
|
78
|
+
# Create source table
|
|
79
|
+
console.print(f"\n[bold]Loaded Sources:[/bold]")
|
|
80
|
+
|
|
81
|
+
if not context.sources:
|
|
82
|
+
console.print(" [dim]No sources loaded[/dim]")
|
|
83
|
+
console.print("\n[dim]Check that .doit/memory/ contains constitution.md or roadmap.md[/dim]")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
table = Table(show_header=True, header_style="bold")
|
|
87
|
+
table.add_column("Source", style="cyan")
|
|
88
|
+
table.add_column("Path", style="dim")
|
|
89
|
+
table.add_column("Tokens", justify="right")
|
|
90
|
+
table.add_column("Status")
|
|
91
|
+
|
|
92
|
+
for source in context.sources:
|
|
93
|
+
# Truncate path for display
|
|
94
|
+
path_str = str(source.path)
|
|
95
|
+
if len(path_str) > 40:
|
|
96
|
+
path_str = "..." + path_str[-37:]
|
|
97
|
+
|
|
98
|
+
# Status indicator
|
|
99
|
+
if source.truncated:
|
|
100
|
+
status = f"[yellow]truncated ({source.original_tokens:,} -> {source.token_count:,})[/yellow]"
|
|
101
|
+
elif source.source_type == "roadmap" and config.summarization.enabled:
|
|
102
|
+
status = "[cyan]summarized[/cyan]"
|
|
103
|
+
elif source.source_type == "completed_roadmap":
|
|
104
|
+
status = "[cyan]formatted[/cyan]"
|
|
105
|
+
else:
|
|
106
|
+
status = "[green]complete[/green]"
|
|
107
|
+
|
|
108
|
+
table.add_row(
|
|
109
|
+
source.source_type,
|
|
110
|
+
path_str,
|
|
111
|
+
f"{source.token_count:,}",
|
|
112
|
+
status,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
|
|
117
|
+
# Summary
|
|
118
|
+
console.print(f"\n[bold]Summary:[/bold]")
|
|
119
|
+
console.print(f" Total sources: {len(context.sources)}")
|
|
120
|
+
console.print(f" Total tokens: {context.total_tokens:,}")
|
|
121
|
+
|
|
122
|
+
if context.any_truncated:
|
|
123
|
+
console.print(" [yellow]Some sources were truncated to fit token limits[/yellow]")
|
|
124
|
+
|
|
125
|
+
# Check for guidance prompt (indicates context was condensed)
|
|
126
|
+
if hasattr(context, '_guidance_prompt') and context._guidance_prompt:
|
|
127
|
+
console.print(" [cyan]AI guidance prompt included (context condensed)[/cyan]")
|
|
128
|
+
|
|
129
|
+
# Show verbose content
|
|
130
|
+
if verbose:
|
|
131
|
+
console.print(f"\n[bold]Source Content Preview:[/bold]")
|
|
132
|
+
for source in context.sources:
|
|
133
|
+
console.print(f"\n[cyan]--- {source.source_type} ---[/cyan]")
|
|
134
|
+
# Show first 500 chars
|
|
135
|
+
preview = source.content[:500]
|
|
136
|
+
if len(source.content) > 500:
|
|
137
|
+
preview += "\n... [truncated for display]"
|
|
138
|
+
console.print(preview)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@context_app.command("status")
|
|
142
|
+
def context_status() -> None:
|
|
143
|
+
"""Show context configuration and file availability."""
|
|
144
|
+
project_root = Path.cwd()
|
|
145
|
+
|
|
146
|
+
console.print("[bold]Context Configuration Status[/bold]")
|
|
147
|
+
console.print("=" * 50)
|
|
148
|
+
|
|
149
|
+
# Check config file
|
|
150
|
+
config_path = project_root / ".doit" / "config" / "context.yaml"
|
|
151
|
+
console.print(f"\n[bold]Configuration:[/bold]")
|
|
152
|
+
|
|
153
|
+
if config_path.exists():
|
|
154
|
+
console.print(f" [green]\u2713[/green] Config file: {config_path}")
|
|
155
|
+
config = ContextConfig.load_from_project(project_root)
|
|
156
|
+
else:
|
|
157
|
+
console.print(f" [dim]\u2717[/dim] Config file: {config_path} [dim](using defaults)[/dim]")
|
|
158
|
+
config = ContextConfig.load_default()
|
|
159
|
+
|
|
160
|
+
# Show source availability
|
|
161
|
+
console.print(f"\n[bold]Source Files:[/bold]")
|
|
162
|
+
|
|
163
|
+
memory_dir = project_root / ".doit" / "memory"
|
|
164
|
+
specs_dir = project_root / "specs"
|
|
165
|
+
|
|
166
|
+
sources = [
|
|
167
|
+
("constitution", memory_dir / "constitution.md"),
|
|
168
|
+
("roadmap", memory_dir / "roadmap.md"),
|
|
169
|
+
("completed_roadmap", memory_dir / "completed_roadmap.md"),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
for name, path in sources:
|
|
173
|
+
source_config = config.get_source_config(name)
|
|
174
|
+
enabled = source_config.enabled if source_config else True
|
|
175
|
+
|
|
176
|
+
if path.exists():
|
|
177
|
+
if enabled:
|
|
178
|
+
console.print(f" [green]\u2713[/green] {name}: {path}")
|
|
179
|
+
else:
|
|
180
|
+
console.print(f" [yellow]\u2713[/yellow] {name}: {path} [dim](disabled)[/dim]")
|
|
181
|
+
else:
|
|
182
|
+
console.print(f" [dim]\u2717[/dim] {name}: {path} [dim](not found)[/dim]")
|
|
183
|
+
|
|
184
|
+
# Check specs directory
|
|
185
|
+
if specs_dir.exists():
|
|
186
|
+
spec_count = len(list(specs_dir.glob("*/")))
|
|
187
|
+
console.print(f" [green]\u2713[/green] specs directory: {spec_count} feature(s)")
|
|
188
|
+
else:
|
|
189
|
+
console.print(f" [dim]\u2717[/dim] specs directory: [dim](not found)[/dim]")
|
|
190
|
+
|
|
191
|
+
# Show current branch info
|
|
192
|
+
console.print(f"\n[bold]Git Branch:[/bold]")
|
|
193
|
+
loader = ContextLoader(project_root=project_root, config=config)
|
|
194
|
+
branch = loader.get_current_branch()
|
|
195
|
+
|
|
196
|
+
if branch:
|
|
197
|
+
console.print(f" Current: {branch}")
|
|
198
|
+
feature = loader.extract_feature_name(branch)
|
|
199
|
+
if feature:
|
|
200
|
+
console.print(f" Feature: {feature}")
|
|
201
|
+
spec_path = specs_dir / feature
|
|
202
|
+
if spec_path.exists():
|
|
203
|
+
console.print(f" [green]\u2713[/green] Current spec: {spec_path}")
|
|
204
|
+
else:
|
|
205
|
+
console.print(f" [dim]\u2717[/dim] Current spec: [dim](no matching spec)[/dim]")
|
|
206
|
+
else:
|
|
207
|
+
console.print(f" [dim]Not a feature branch (pattern: NNN-feature-name)[/dim]")
|
|
208
|
+
else:
|
|
209
|
+
console.print(f" [dim]Not in a git repository or git not available[/dim]")
|
|
210
|
+
|
|
211
|
+
# Usage hints
|
|
212
|
+
console.print(f"\n[dim]Use 'doit context show' to see loaded context[/dim]")
|
|
213
|
+
console.print(f"[dim]Use 'doit context show --command specit' to see command-specific context[/dim]")
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""CLI commands for diagram generation."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from ..models.diagram_models import DiagramType
|
|
11
|
+
from ..services.diagram_service import DiagramService
|
|
12
|
+
|
|
13
|
+
# Create Typer app for diagram subcommands
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="diagram",
|
|
16
|
+
help="Generate and validate Mermaid diagrams from specifications",
|
|
17
|
+
add_completion=False,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resolve_file_path(file: Optional[Path]) -> Optional[Path]:
|
|
24
|
+
"""Resolve file path, auto-detecting if not provided.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
file: Optional path to spec/plan file
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Resolved path or None if not found
|
|
31
|
+
"""
|
|
32
|
+
if file:
|
|
33
|
+
return file.resolve()
|
|
34
|
+
|
|
35
|
+
# Try to auto-detect spec.md in current directory or specs/
|
|
36
|
+
cwd = Path.cwd()
|
|
37
|
+
|
|
38
|
+
# Check current directory
|
|
39
|
+
if (cwd / "spec.md").exists():
|
|
40
|
+
return cwd / "spec.md"
|
|
41
|
+
|
|
42
|
+
# Check for specs/ directory with current feature
|
|
43
|
+
specs_dir = cwd / "specs"
|
|
44
|
+
if specs_dir.exists():
|
|
45
|
+
# Look for most recently modified spec.md
|
|
46
|
+
spec_files = list(specs_dir.glob("*/spec.md"))
|
|
47
|
+
if spec_files:
|
|
48
|
+
return max(spec_files, key=lambda p: p.stat().st_mtime)
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_diagram_types(type_str: str) -> list[DiagramType]:
|
|
54
|
+
"""Parse diagram type string to list of DiagramType.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
type_str: Type string (user-journey, er-diagram, architecture, all)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of DiagramType values
|
|
61
|
+
"""
|
|
62
|
+
if type_str == "all":
|
|
63
|
+
return [DiagramType.USER_JOURNEY, DiagramType.ER_DIAGRAM]
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
return [DiagramType.from_string(type_str)]
|
|
67
|
+
except ValueError:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command(name="generate")
|
|
72
|
+
def generate_command(
|
|
73
|
+
file: Optional[Path] = typer.Argument(
|
|
74
|
+
None,
|
|
75
|
+
help="Path to spec.md or plan.md file",
|
|
76
|
+
exists=False,
|
|
77
|
+
),
|
|
78
|
+
strict: bool = typer.Option(
|
|
79
|
+
False,
|
|
80
|
+
"--strict",
|
|
81
|
+
"-s",
|
|
82
|
+
help="Fail on validation errors",
|
|
83
|
+
),
|
|
84
|
+
no_insert: bool = typer.Option(
|
|
85
|
+
False,
|
|
86
|
+
"--no-insert",
|
|
87
|
+
help="Output diagrams without inserting into file",
|
|
88
|
+
),
|
|
89
|
+
diagram_type: str = typer.Option(
|
|
90
|
+
"all",
|
|
91
|
+
"--type",
|
|
92
|
+
"-t",
|
|
93
|
+
help="Diagram type: user-journey, er-diagram, architecture, all",
|
|
94
|
+
),
|
|
95
|
+
output: Optional[Path] = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"--output",
|
|
98
|
+
"-o",
|
|
99
|
+
help="Write diagrams to separate file",
|
|
100
|
+
),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Generate Mermaid diagrams for a specification file.
|
|
103
|
+
|
|
104
|
+
If FILE is not provided, auto-detects spec.md in current directory
|
|
105
|
+
or finds the most recently modified spec in specs/.
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
doit diagram generate
|
|
109
|
+
doit diagram generate specs/035-feature/spec.md
|
|
110
|
+
doit diagram generate --type er-diagram --strict
|
|
111
|
+
doit diagram generate --no-insert --output diagrams.md
|
|
112
|
+
"""
|
|
113
|
+
# Resolve file path
|
|
114
|
+
resolved_path = _resolve_file_path(file)
|
|
115
|
+
if not resolved_path:
|
|
116
|
+
console.print("[red]Error:[/red] No spec file found. Provide a path or run from a spec directory.")
|
|
117
|
+
raise typer.Exit(code=1)
|
|
118
|
+
|
|
119
|
+
if not resolved_path.exists():
|
|
120
|
+
console.print(f"[red]Error:[/red] File not found: {resolved_path}")
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
|
|
123
|
+
# Parse diagram types
|
|
124
|
+
types = _parse_diagram_types(diagram_type)
|
|
125
|
+
if not types and diagram_type != "all":
|
|
126
|
+
console.print(f"[red]Error:[/red] Unknown diagram type: {diagram_type}")
|
|
127
|
+
console.print("Valid types: user-journey, er-diagram, architecture, all")
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
console.print(f"Generating diagrams for: [cyan]{resolved_path}[/cyan]")
|
|
131
|
+
console.print()
|
|
132
|
+
|
|
133
|
+
# Create service and generate
|
|
134
|
+
service = DiagramService(strict=strict, backup=True)
|
|
135
|
+
result = service.generate(
|
|
136
|
+
file_path=resolved_path,
|
|
137
|
+
diagram_types=types if types else None,
|
|
138
|
+
insert=not no_insert,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Handle errors
|
|
142
|
+
if not result.success:
|
|
143
|
+
console.print(f"[red]Error:[/red] {result.error}")
|
|
144
|
+
raise typer.Exit(code=2 if strict else 1)
|
|
145
|
+
|
|
146
|
+
# Display results table
|
|
147
|
+
if result.diagrams:
|
|
148
|
+
table = Table(show_header=True, header_style="bold")
|
|
149
|
+
table.add_column("Diagram Type")
|
|
150
|
+
table.add_column("Status")
|
|
151
|
+
table.add_column("Nodes")
|
|
152
|
+
|
|
153
|
+
for diagram in result.diagrams:
|
|
154
|
+
status = "[green]✅ Generated[/green]" if diagram.is_valid else "[yellow]⚠️ Generated (warnings)[/yellow]"
|
|
155
|
+
table.add_row(
|
|
156
|
+
diagram.diagram_type.value,
|
|
157
|
+
status,
|
|
158
|
+
str(diagram.node_count),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
console.print(table)
|
|
162
|
+
console.print()
|
|
163
|
+
|
|
164
|
+
# Show validation warnings
|
|
165
|
+
for diagram in result.diagrams:
|
|
166
|
+
if diagram.validation and diagram.validation.warnings:
|
|
167
|
+
console.print(f"[yellow]Warnings for {diagram.diagram_type.value}:[/yellow]")
|
|
168
|
+
for warning in diagram.validation.warnings:
|
|
169
|
+
console.print(f" • {warning}")
|
|
170
|
+
console.print()
|
|
171
|
+
|
|
172
|
+
# Output to file if requested
|
|
173
|
+
if output:
|
|
174
|
+
output_content = []
|
|
175
|
+
for diagram in result.diagrams:
|
|
176
|
+
output_content.append(f"## {diagram.diagram_type.value}\n")
|
|
177
|
+
output_content.append(diagram.wrapped_content)
|
|
178
|
+
output_content.append("\n")
|
|
179
|
+
|
|
180
|
+
output.write_text("\n".join(output_content), encoding="utf-8")
|
|
181
|
+
console.print(f"[green]✅[/green] Diagrams written to: {output}")
|
|
182
|
+
elif not no_insert and result.sections_updated:
|
|
183
|
+
console.print(f"[green]✅[/green] Diagrams inserted into {resolved_path.name}")
|
|
184
|
+
console.print(f" Sections updated: {', '.join(result.sections_updated)}")
|
|
185
|
+
elif no_insert:
|
|
186
|
+
# Print diagram content to stdout
|
|
187
|
+
for diagram in result.diagrams:
|
|
188
|
+
console.print(f"\n[bold]## {diagram.diagram_type.value}[/bold]\n")
|
|
189
|
+
console.print(diagram.wrapped_content)
|
|
190
|
+
else:
|
|
191
|
+
console.print("[yellow]No diagrams generated.[/yellow] Check that the spec has User Stories or Key Entities.")
|
|
192
|
+
raise typer.Exit(code=3)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command(name="validate")
|
|
196
|
+
def validate_command(
|
|
197
|
+
file: Optional[Path] = typer.Argument(
|
|
198
|
+
None,
|
|
199
|
+
help="Path to file containing Mermaid diagrams",
|
|
200
|
+
exists=False,
|
|
201
|
+
),
|
|
202
|
+
strict: bool = typer.Option(
|
|
203
|
+
False,
|
|
204
|
+
"--strict",
|
|
205
|
+
"-s",
|
|
206
|
+
help="Use stricter validation rules",
|
|
207
|
+
),
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Validate Mermaid diagrams in a file.
|
|
210
|
+
|
|
211
|
+
Checks all Mermaid code blocks in the file for syntax errors.
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
doit diagram validate spec.md
|
|
215
|
+
doit diagram validate --strict
|
|
216
|
+
"""
|
|
217
|
+
import re
|
|
218
|
+
|
|
219
|
+
# Resolve file path
|
|
220
|
+
resolved_path = _resolve_file_path(file)
|
|
221
|
+
if not resolved_path:
|
|
222
|
+
console.print("[red]Error:[/red] No file found to validate.")
|
|
223
|
+
raise typer.Exit(code=2)
|
|
224
|
+
|
|
225
|
+
if not resolved_path.exists():
|
|
226
|
+
console.print(f"[red]Error:[/red] File not found: {resolved_path}")
|
|
227
|
+
raise typer.Exit(code=2)
|
|
228
|
+
|
|
229
|
+
console.print(f"Validating diagrams in: [cyan]{resolved_path}[/cyan]")
|
|
230
|
+
console.print()
|
|
231
|
+
|
|
232
|
+
content = resolved_path.read_text(encoding="utf-8")
|
|
233
|
+
|
|
234
|
+
# Find all mermaid code blocks
|
|
235
|
+
mermaid_pattern = re.compile(r"```mermaid\s*\n(.*?)\n```", re.DOTALL)
|
|
236
|
+
matches = list(mermaid_pattern.finditer(content))
|
|
237
|
+
|
|
238
|
+
if not matches:
|
|
239
|
+
console.print("[yellow]No Mermaid diagrams found in file.[/yellow]")
|
|
240
|
+
raise typer.Exit(code=0)
|
|
241
|
+
|
|
242
|
+
from ..services.mermaid_validator import MermaidValidator
|
|
243
|
+
|
|
244
|
+
validator = MermaidValidator()
|
|
245
|
+
has_errors = False
|
|
246
|
+
|
|
247
|
+
table = Table(show_header=True, header_style="bold")
|
|
248
|
+
table.add_column("Diagram #")
|
|
249
|
+
table.add_column("Type")
|
|
250
|
+
table.add_column("Status")
|
|
251
|
+
table.add_column("Issues")
|
|
252
|
+
|
|
253
|
+
for i, match in enumerate(matches, start=1):
|
|
254
|
+
diagram_content = match.group(1)
|
|
255
|
+
result = validator.validate(diagram_content)
|
|
256
|
+
|
|
257
|
+
# Determine type
|
|
258
|
+
if "erDiagram" in diagram_content.lower():
|
|
259
|
+
diagram_type = "ER Diagram"
|
|
260
|
+
elif "flowchart" in diagram_content.lower():
|
|
261
|
+
diagram_type = "Flowchart"
|
|
262
|
+
else:
|
|
263
|
+
diagram_type = "Unknown"
|
|
264
|
+
|
|
265
|
+
if result.passed and not result.warnings:
|
|
266
|
+
status = "[green]✅ Valid[/green]"
|
|
267
|
+
issues = "None"
|
|
268
|
+
elif result.passed:
|
|
269
|
+
status = "[yellow]⚠️ Warning[/yellow]"
|
|
270
|
+
issues = f"{result.warning_count} warnings"
|
|
271
|
+
else:
|
|
272
|
+
status = "[red]❌ Invalid[/red]"
|
|
273
|
+
issues = f"{result.error_count} errors"
|
|
274
|
+
has_errors = True
|
|
275
|
+
|
|
276
|
+
table.add_row(str(i), diagram_type, status, issues)
|
|
277
|
+
|
|
278
|
+
console.print(table)
|
|
279
|
+
|
|
280
|
+
# Show detailed errors/warnings
|
|
281
|
+
for i, match in enumerate(matches, start=1):
|
|
282
|
+
diagram_content = match.group(1)
|
|
283
|
+
result = validator.validate(diagram_content)
|
|
284
|
+
|
|
285
|
+
if result.errors:
|
|
286
|
+
console.print(f"\n[red]Errors in diagram {i}:[/red]")
|
|
287
|
+
for error in result.errors:
|
|
288
|
+
console.print(f" • {error}")
|
|
289
|
+
|
|
290
|
+
if result.warnings:
|
|
291
|
+
console.print(f"\n[yellow]Warnings in diagram {i}:[/yellow]")
|
|
292
|
+
for warning in result.warnings:
|
|
293
|
+
console.print(f" • {warning}")
|
|
294
|
+
|
|
295
|
+
if has_errors:
|
|
296
|
+
console.print("\n[red]Validation failed.[/red]")
|
|
297
|
+
raise typer.Exit(code=1)
|
|
298
|
+
else:
|
|
299
|
+
console.print("\n[green]All diagrams valid.[/green]")
|
|
300
|
+
raise typer.Exit(code=0)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Export the app for registration
|
|
304
|
+
diagram_app = app
|