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,334 @@
|
|
|
1
|
+
"""Validator for Mermaid diagram syntax."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ..models.diagram_models import DiagramType, ValidationResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MermaidValidator:
|
|
10
|
+
"""Validates Mermaid diagram syntax.
|
|
11
|
+
|
|
12
|
+
Provides regex-based validation for common syntax errors
|
|
13
|
+
in flowchart and erDiagram types.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Valid diagram type declarations
|
|
17
|
+
DIAGRAM_TYPE_PATTERNS = {
|
|
18
|
+
DiagramType.USER_JOURNEY: re.compile(r"^\s*flowchart\s+(LR|TB|RL|BT)", re.MULTILINE),
|
|
19
|
+
DiagramType.ER_DIAGRAM: re.compile(r"^\s*erDiagram\s*$", re.MULTILINE),
|
|
20
|
+
DiagramType.ARCHITECTURE: re.compile(r"^\s*flowchart\s+(LR|TB|RL|BT)", re.MULTILINE),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Valid node ID pattern (alphanumeric with underscores)
|
|
24
|
+
NODE_ID_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$")
|
|
25
|
+
|
|
26
|
+
# Arrow patterns for flowchart
|
|
27
|
+
FLOWCHART_ARROW_PATTERNS = [
|
|
28
|
+
re.compile(r"-->"), # Standard arrow
|
|
29
|
+
re.compile(r"-\.->"), # Dotted arrow
|
|
30
|
+
re.compile(r"==>"), # Thick arrow
|
|
31
|
+
re.compile(r"--\s*\w+\s*-->"), # Labeled arrow
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# ER diagram cardinality patterns
|
|
35
|
+
ER_CARDINALITY_PATTERN = re.compile(
|
|
36
|
+
r"(\|o|\|\||\}o|\}|)\s*--\s*(\|o|\|\||\}o|\}||\{o|\{)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Bracket pairs for balance checking
|
|
40
|
+
BRACKET_PAIRS = [("{", "}"), ("[", "]"), ("(", ")")]
|
|
41
|
+
|
|
42
|
+
def validate(
|
|
43
|
+
self, content: str, diagram_type: Optional[DiagramType] = None
|
|
44
|
+
) -> ValidationResult:
|
|
45
|
+
"""Validate Mermaid diagram syntax.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
content: Mermaid diagram content (without code fences)
|
|
49
|
+
diagram_type: Expected diagram type (auto-detected if None)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
ValidationResult with pass/fail and any issues found
|
|
53
|
+
"""
|
|
54
|
+
errors: list[str] = []
|
|
55
|
+
warnings: list[str] = []
|
|
56
|
+
|
|
57
|
+
# Strip code fences if present
|
|
58
|
+
content = self._strip_code_fences(content)
|
|
59
|
+
|
|
60
|
+
if not content.strip():
|
|
61
|
+
errors.append("Empty diagram content")
|
|
62
|
+
return ValidationResult(passed=False, errors=errors, warnings=warnings)
|
|
63
|
+
|
|
64
|
+
# Auto-detect diagram type if not provided
|
|
65
|
+
if diagram_type is None:
|
|
66
|
+
diagram_type = self._detect_diagram_type(content)
|
|
67
|
+
|
|
68
|
+
if diagram_type is None:
|
|
69
|
+
errors.append("Could not detect diagram type - missing type declaration")
|
|
70
|
+
return ValidationResult(passed=False, errors=errors, warnings=warnings)
|
|
71
|
+
|
|
72
|
+
# Validate diagram type declaration
|
|
73
|
+
type_errors = self._validate_type_declaration(content, diagram_type)
|
|
74
|
+
errors.extend(type_errors)
|
|
75
|
+
|
|
76
|
+
# Check bracket balance (skip for ER diagrams due to cardinality notation)
|
|
77
|
+
if diagram_type != DiagramType.ER_DIAGRAM:
|
|
78
|
+
bracket_errors = self._validate_brackets(content)
|
|
79
|
+
errors.extend(bracket_errors)
|
|
80
|
+
|
|
81
|
+
# Type-specific validation
|
|
82
|
+
if diagram_type in (DiagramType.USER_JOURNEY, DiagramType.ARCHITECTURE):
|
|
83
|
+
flowchart_errors, flowchart_warnings = self._validate_flowchart(content)
|
|
84
|
+
errors.extend(flowchart_errors)
|
|
85
|
+
warnings.extend(flowchart_warnings)
|
|
86
|
+
elif diagram_type == DiagramType.ER_DIAGRAM:
|
|
87
|
+
er_errors, er_warnings = self._validate_er_diagram(content)
|
|
88
|
+
errors.extend(er_errors)
|
|
89
|
+
warnings.extend(er_warnings)
|
|
90
|
+
|
|
91
|
+
# Check for node count warning
|
|
92
|
+
node_count = self._count_nodes(content, diagram_type)
|
|
93
|
+
if node_count > 20:
|
|
94
|
+
warnings.append(f"Diagram has {node_count} nodes - may not render well in GitHub")
|
|
95
|
+
|
|
96
|
+
passed = len(errors) == 0
|
|
97
|
+
return ValidationResult(passed=passed, errors=errors, warnings=warnings)
|
|
98
|
+
|
|
99
|
+
def _strip_code_fences(self, content: str) -> str:
|
|
100
|
+
"""Remove markdown code fences from content.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
content: Content possibly wrapped in code fences
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Content without code fences
|
|
107
|
+
"""
|
|
108
|
+
lines = content.strip().split("\n")
|
|
109
|
+
|
|
110
|
+
# Check for opening fence
|
|
111
|
+
if lines and lines[0].strip().startswith("```"):
|
|
112
|
+
lines = lines[1:]
|
|
113
|
+
|
|
114
|
+
# Check for closing fence
|
|
115
|
+
if lines and lines[-1].strip() == "```":
|
|
116
|
+
lines = lines[:-1]
|
|
117
|
+
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
def _detect_diagram_type(self, content: str) -> Optional[DiagramType]:
|
|
121
|
+
"""Auto-detect diagram type from content.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
content: Diagram content
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Detected DiagramType or None
|
|
128
|
+
"""
|
|
129
|
+
content_lower = content.lower()
|
|
130
|
+
|
|
131
|
+
if "erdiagram" in content_lower:
|
|
132
|
+
return DiagramType.ER_DIAGRAM
|
|
133
|
+
if "flowchart" in content_lower:
|
|
134
|
+
return DiagramType.USER_JOURNEY # Default flowchart to user journey
|
|
135
|
+
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def _validate_type_declaration(
|
|
139
|
+
self, content: str, diagram_type: DiagramType
|
|
140
|
+
) -> list[str]:
|
|
141
|
+
"""Validate diagram type declaration.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
content: Diagram content
|
|
145
|
+
diagram_type: Expected type
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of error messages
|
|
149
|
+
"""
|
|
150
|
+
errors = []
|
|
151
|
+
pattern = self.DIAGRAM_TYPE_PATTERNS.get(diagram_type)
|
|
152
|
+
|
|
153
|
+
if pattern and not pattern.search(content):
|
|
154
|
+
errors.append(f"Missing or invalid diagram type declaration for {diagram_type.value}")
|
|
155
|
+
|
|
156
|
+
return errors
|
|
157
|
+
|
|
158
|
+
def _validate_brackets(self, content: str) -> list[str]:
|
|
159
|
+
"""Check for balanced brackets.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
content: Diagram content
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of error messages for unbalanced brackets
|
|
166
|
+
"""
|
|
167
|
+
errors = []
|
|
168
|
+
|
|
169
|
+
for open_char, close_char in self.BRACKET_PAIRS:
|
|
170
|
+
open_count = content.count(open_char)
|
|
171
|
+
close_count = content.count(close_char)
|
|
172
|
+
|
|
173
|
+
if open_count != close_count:
|
|
174
|
+
errors.append(
|
|
175
|
+
f"Unbalanced brackets: {open_count} '{open_char}' vs {close_count} '{close_char}'"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return errors
|
|
179
|
+
|
|
180
|
+
def _validate_flowchart(self, content: str) -> tuple[list[str], list[str]]:
|
|
181
|
+
"""Validate flowchart-specific syntax.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
content: Flowchart content
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Tuple of (errors, warnings)
|
|
188
|
+
"""
|
|
189
|
+
errors = []
|
|
190
|
+
warnings = []
|
|
191
|
+
|
|
192
|
+
lines = content.split("\n")
|
|
193
|
+
|
|
194
|
+
for line_num, line in enumerate(lines, start=1):
|
|
195
|
+
stripped = line.strip()
|
|
196
|
+
|
|
197
|
+
# Skip empty lines and comments
|
|
198
|
+
if not stripped or stripped.startswith("%%"):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
# Skip subgraph declarations
|
|
202
|
+
if stripped.startswith("subgraph") or stripped == "end":
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Skip the flowchart declaration
|
|
206
|
+
if stripped.startswith("flowchart"):
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Check for node definitions with invalid IDs
|
|
210
|
+
node_match = re.match(r"^\s*([A-Za-z0-9_]+)\s*[\[\(\{]", line)
|
|
211
|
+
if node_match:
|
|
212
|
+
node_id = node_match.group(1)
|
|
213
|
+
if not self.NODE_ID_PATTERN.match(node_id):
|
|
214
|
+
errors.append(f"Line {line_num}: Invalid node ID '{node_id}'")
|
|
215
|
+
|
|
216
|
+
# Check for empty labels
|
|
217
|
+
if '[""]' in line or "[' ']" in line:
|
|
218
|
+
warnings.append(f"Line {line_num}: Empty node label")
|
|
219
|
+
|
|
220
|
+
return errors, warnings
|
|
221
|
+
|
|
222
|
+
def _validate_er_diagram(self, content: str) -> tuple[list[str], list[str]]:
|
|
223
|
+
"""Validate ER diagram-specific syntax.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
content: ER diagram content
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple of (errors, warnings)
|
|
230
|
+
"""
|
|
231
|
+
errors = []
|
|
232
|
+
warnings = []
|
|
233
|
+
|
|
234
|
+
lines = content.split("\n")
|
|
235
|
+
entities_defined: set[str] = set()
|
|
236
|
+
entities_referenced: set[str] = set()
|
|
237
|
+
|
|
238
|
+
for line_num, line in enumerate(lines, start=1):
|
|
239
|
+
stripped = line.strip()
|
|
240
|
+
|
|
241
|
+
# Skip empty lines and comments
|
|
242
|
+
if not stripped or stripped.startswith("%%"):
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Skip erDiagram declaration
|
|
246
|
+
if stripped.lower() == "erdiagram":
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Check for entity definition (entity {)
|
|
250
|
+
entity_def_match = re.match(r"^\s*([A-Za-z][A-Za-z0-9_]*)\s*\{", line)
|
|
251
|
+
if entity_def_match:
|
|
252
|
+
entities_defined.add(entity_def_match.group(1))
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Check for relationship line
|
|
256
|
+
rel_match = re.match(
|
|
257
|
+
r"^\s*([A-Za-z][A-Za-z0-9_]*)\s+\S+\s+([A-Za-z][A-Za-z0-9_]*)", line
|
|
258
|
+
)
|
|
259
|
+
if rel_match:
|
|
260
|
+
entities_referenced.add(rel_match.group(1))
|
|
261
|
+
entities_referenced.add(rel_match.group(2))
|
|
262
|
+
|
|
263
|
+
# Validate cardinality notation
|
|
264
|
+
if not self._validate_cardinality_in_line(line):
|
|
265
|
+
warnings.append(
|
|
266
|
+
f"Line {line_num}: Potentially invalid cardinality notation"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Check for orphan entities (defined but no relationships)
|
|
270
|
+
orphans = entities_defined - entities_referenced
|
|
271
|
+
for orphan in orphans:
|
|
272
|
+
warnings.append(f"Orphan entity: '{orphan}' has no relationships")
|
|
273
|
+
|
|
274
|
+
return errors, warnings
|
|
275
|
+
|
|
276
|
+
def _validate_cardinality_in_line(self, line: str) -> bool:
|
|
277
|
+
"""Check if a line has valid cardinality notation.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
line: Relationship line
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if valid, False if potentially invalid
|
|
284
|
+
"""
|
|
285
|
+
# Valid cardinality patterns
|
|
286
|
+
valid_patterns = [
|
|
287
|
+
"||--||", # One to one
|
|
288
|
+
"||--o{", # One to many
|
|
289
|
+
"}o--||", # Many to one
|
|
290
|
+
"}o--o{", # Many to many
|
|
291
|
+
"|o--||", # Zero or one to one
|
|
292
|
+
"||--o|", # One to zero or one
|
|
293
|
+
"|o--o{", # Zero or one to many
|
|
294
|
+
"}o--o|", # Zero or many to one
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
for pattern in valid_patterns:
|
|
298
|
+
if pattern in line:
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
# If line has -- but no valid pattern, might be invalid
|
|
302
|
+
if "--" in line and ":" in line:
|
|
303
|
+
return True # Assume valid if it has relationship structure
|
|
304
|
+
|
|
305
|
+
return True # Be permissive for edge cases
|
|
306
|
+
|
|
307
|
+
def _count_nodes(self, content: str, diagram_type: DiagramType) -> int:
|
|
308
|
+
"""Count approximate number of nodes in diagram.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
content: Diagram content
|
|
312
|
+
diagram_type: Type of diagram
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Approximate node count
|
|
316
|
+
"""
|
|
317
|
+
if diagram_type == DiagramType.ER_DIAGRAM:
|
|
318
|
+
# Count entity definitions
|
|
319
|
+
return len(re.findall(r"^\s*[A-Za-z][A-Za-z0-9_]*\s*\{", content, re.MULTILINE))
|
|
320
|
+
else:
|
|
321
|
+
# Count node definitions in flowchart
|
|
322
|
+
return len(re.findall(r"^\s*[A-Za-z][A-Za-z0-9_]+\s*[\[\(\{]", content, re.MULTILINE))
|
|
323
|
+
|
|
324
|
+
def validate_quick(self, content: str) -> bool:
|
|
325
|
+
"""Quick validation - just check for basic validity.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
content: Diagram content
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
True if diagram appears valid
|
|
332
|
+
"""
|
|
333
|
+
result = self.validate(content)
|
|
334
|
+
return result.passed
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Service to transform command templates to GitHub Copilot prompt format."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..models.sync_models import CommandTemplate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PromptTransformer:
|
|
9
|
+
"""Transforms doit command templates to GitHub Copilot prompt format."""
|
|
10
|
+
|
|
11
|
+
def transform(self, template: CommandTemplate) -> str:
|
|
12
|
+
"""Transform a command template to Copilot prompt format.
|
|
13
|
+
|
|
14
|
+
Transformation rules (from research.md):
|
|
15
|
+
- Remove YAML frontmatter entirely (Copilot prompts are plain markdown)
|
|
16
|
+
- Replace $ARGUMENTS with natural language
|
|
17
|
+
- Preserve ## Outline, ## Key Rules, and other sections
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
template: The command template to transform.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Transformed content suitable for Copilot prompt file.
|
|
24
|
+
"""
|
|
25
|
+
content = template.content
|
|
26
|
+
|
|
27
|
+
# Step 1: Strip YAML frontmatter
|
|
28
|
+
content = self._strip_yaml_frontmatter(content)
|
|
29
|
+
|
|
30
|
+
# Step 2: Add description as header if available
|
|
31
|
+
if template.description:
|
|
32
|
+
header = f"# {self._title_from_name(template.name)}\n\n{template.description}\n\n"
|
|
33
|
+
content = header + content.lstrip()
|
|
34
|
+
|
|
35
|
+
# Step 3: Replace $ARGUMENTS placeholder with natural language
|
|
36
|
+
content = self._replace_arguments_placeholder(content)
|
|
37
|
+
|
|
38
|
+
return content.strip() + "\n"
|
|
39
|
+
|
|
40
|
+
def _strip_yaml_frontmatter(self, content: str) -> str:
|
|
41
|
+
"""Remove YAML frontmatter from content.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
content: Markdown content potentially with YAML frontmatter.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Content with frontmatter removed.
|
|
48
|
+
"""
|
|
49
|
+
if not content.startswith("---"):
|
|
50
|
+
return content
|
|
51
|
+
|
|
52
|
+
# Find the closing ---
|
|
53
|
+
try:
|
|
54
|
+
end_idx = content.index("---", 3)
|
|
55
|
+
# Skip the closing --- and any trailing newline
|
|
56
|
+
return content[end_idx + 3:].lstrip("\n")
|
|
57
|
+
except ValueError:
|
|
58
|
+
# No closing ---, return original
|
|
59
|
+
return content
|
|
60
|
+
|
|
61
|
+
def _replace_arguments_placeholder(self, content: str) -> str:
|
|
62
|
+
"""Replace $ARGUMENTS placeholder with natural language.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
content: Content potentially containing $ARGUMENTS.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Content with placeholder replaced.
|
|
69
|
+
"""
|
|
70
|
+
# Replace the code block containing $ARGUMENTS
|
|
71
|
+
pattern = r"```text\s*\n\$ARGUMENTS\s*\n```"
|
|
72
|
+
replacement = "Consider any arguments or options the user provides."
|
|
73
|
+
content = re.sub(pattern, replacement, content)
|
|
74
|
+
|
|
75
|
+
# Also replace standalone $ARGUMENTS references
|
|
76
|
+
content = content.replace("$ARGUMENTS", "the user's input")
|
|
77
|
+
|
|
78
|
+
return content
|
|
79
|
+
|
|
80
|
+
def _title_from_name(self, name: str) -> str:
|
|
81
|
+
"""Convert command name to title case.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
name: Command name like "doit.checkin".
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Title like "Doit Checkin".
|
|
88
|
+
"""
|
|
89
|
+
# doit.checkin -> Doit Checkin
|
|
90
|
+
parts = name.replace("doit.", "").split(".")
|
|
91
|
+
return "Doit " + " ".join(p.title() for p in parts)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Service to write GitHub Copilot prompt files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..models.sync_models import (
|
|
6
|
+
CommandTemplate,
|
|
7
|
+
FileOperation,
|
|
8
|
+
OperationType,
|
|
9
|
+
SyncResult,
|
|
10
|
+
)
|
|
11
|
+
from .prompt_transformer import PromptTransformer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PromptWriter:
|
|
15
|
+
"""Writes transformed prompts to .github/prompts/ directory."""
|
|
16
|
+
|
|
17
|
+
DEFAULT_PROMPTS_DIR = ".github/prompts"
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
project_root: Path | None = None,
|
|
22
|
+
transformer: PromptTransformer | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize the prompt writer.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_root: Root directory of the project. Defaults to current directory.
|
|
28
|
+
transformer: Transformer to use. Defaults to new PromptTransformer.
|
|
29
|
+
"""
|
|
30
|
+
self.project_root = project_root or Path.cwd()
|
|
31
|
+
self.prompts_dir = self.project_root / self.DEFAULT_PROMPTS_DIR
|
|
32
|
+
self.transformer = transformer or PromptTransformer()
|
|
33
|
+
|
|
34
|
+
def get_prompts_directory(self) -> Path:
|
|
35
|
+
"""Get the prompts directory path."""
|
|
36
|
+
return self.prompts_dir
|
|
37
|
+
|
|
38
|
+
def ensure_prompts_directory(self) -> None:
|
|
39
|
+
"""Ensure the prompts directory exists."""
|
|
40
|
+
self.prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
def get_prompt_path(self, template: CommandTemplate) -> Path:
|
|
43
|
+
"""Get the output path for a prompt file.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
template: The command template.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Path where the prompt file should be written.
|
|
50
|
+
"""
|
|
51
|
+
return self.prompts_dir / template.prompt_filename
|
|
52
|
+
|
|
53
|
+
def write_prompt(
|
|
54
|
+
self,
|
|
55
|
+
template: CommandTemplate,
|
|
56
|
+
force: bool = False,
|
|
57
|
+
) -> FileOperation:
|
|
58
|
+
"""Write a prompt file from a command template.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
template: The command template to transform and write.
|
|
62
|
+
force: If True, overwrite even if file exists and is up-to-date.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
FileOperation describing what happened.
|
|
66
|
+
"""
|
|
67
|
+
prompt_path = self.get_prompt_path(template)
|
|
68
|
+
prompt_path_str = str(prompt_path)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# Check if prompt already exists and is up-to-date
|
|
72
|
+
if prompt_path.exists() and not force:
|
|
73
|
+
prompt_mtime = prompt_path.stat().st_mtime
|
|
74
|
+
template_mtime = template.modified_at.timestamp()
|
|
75
|
+
|
|
76
|
+
if prompt_mtime >= template_mtime:
|
|
77
|
+
return FileOperation(
|
|
78
|
+
file_path=prompt_path_str,
|
|
79
|
+
operation_type=OperationType.SKIPPED,
|
|
80
|
+
success=True,
|
|
81
|
+
message="Already up-to-date",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Transform content
|
|
85
|
+
content = self.transformer.transform(template)
|
|
86
|
+
|
|
87
|
+
# Ensure directory exists
|
|
88
|
+
self.ensure_prompts_directory()
|
|
89
|
+
|
|
90
|
+
# Determine operation type
|
|
91
|
+
operation_type = (
|
|
92
|
+
OperationType.UPDATED if prompt_path.exists() else OperationType.CREATED
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Write file
|
|
96
|
+
prompt_path.write_text(content, encoding="utf-8")
|
|
97
|
+
|
|
98
|
+
return FileOperation(
|
|
99
|
+
file_path=prompt_path_str,
|
|
100
|
+
operation_type=operation_type,
|
|
101
|
+
success=True,
|
|
102
|
+
message=f"{operation_type.value.title()} successfully",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
106
|
+
return FileOperation(
|
|
107
|
+
file_path=prompt_path_str,
|
|
108
|
+
operation_type=OperationType.FAILED,
|
|
109
|
+
success=False,
|
|
110
|
+
message=str(e),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def write_prompts(
|
|
114
|
+
self,
|
|
115
|
+
templates: list[CommandTemplate],
|
|
116
|
+
force: bool = False,
|
|
117
|
+
) -> SyncResult:
|
|
118
|
+
"""Write prompt files for multiple command templates.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
templates: List of command templates to process.
|
|
122
|
+
force: If True, overwrite even if files are up-to-date.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
SyncResult with summary of all operations.
|
|
126
|
+
"""
|
|
127
|
+
result = SyncResult(total_commands=len(templates))
|
|
128
|
+
|
|
129
|
+
for template in templates:
|
|
130
|
+
operation = self.write_prompt(template, force=force)
|
|
131
|
+
result.add_operation(operation)
|
|
132
|
+
|
|
133
|
+
return result
|