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.

Files changed (135) hide show
  1. doit_cli/__init__.py +1356 -0
  2. doit_cli/cli/__init__.py +26 -0
  3. doit_cli/cli/analytics_command.py +616 -0
  4. doit_cli/cli/context_command.py +213 -0
  5. doit_cli/cli/diagram_command.py +304 -0
  6. doit_cli/cli/fixit_command.py +641 -0
  7. doit_cli/cli/hooks_command.py +211 -0
  8. doit_cli/cli/init_command.py +613 -0
  9. doit_cli/cli/memory_command.py +293 -0
  10. doit_cli/cli/roadmapit_command.py +10 -0
  11. doit_cli/cli/status_command.py +117 -0
  12. doit_cli/cli/sync_prompts_command.py +248 -0
  13. doit_cli/cli/validate_command.py +196 -0
  14. doit_cli/cli/verify_command.py +204 -0
  15. doit_cli/cli/workflow_mixin.py +224 -0
  16. doit_cli/cli/xref_command.py +555 -0
  17. doit_cli/formatters/__init__.py +8 -0
  18. doit_cli/formatters/base.py +38 -0
  19. doit_cli/formatters/json_formatter.py +126 -0
  20. doit_cli/formatters/markdown_formatter.py +97 -0
  21. doit_cli/formatters/rich_formatter.py +257 -0
  22. doit_cli/main.py +51 -0
  23. doit_cli/models/__init__.py +139 -0
  24. doit_cli/models/agent.py +74 -0
  25. doit_cli/models/analytics_models.py +384 -0
  26. doit_cli/models/context_config.py +464 -0
  27. doit_cli/models/crossref_models.py +182 -0
  28. doit_cli/models/diagram_models.py +363 -0
  29. doit_cli/models/fixit_models.py +355 -0
  30. doit_cli/models/hook_config.py +125 -0
  31. doit_cli/models/project.py +91 -0
  32. doit_cli/models/results.py +121 -0
  33. doit_cli/models/search_models.py +228 -0
  34. doit_cli/models/status_models.py +195 -0
  35. doit_cli/models/sync_models.py +146 -0
  36. doit_cli/models/template.py +77 -0
  37. doit_cli/models/validation_models.py +175 -0
  38. doit_cli/models/workflow_models.py +319 -0
  39. doit_cli/prompts/__init__.py +5 -0
  40. doit_cli/prompts/fixit_prompts.py +344 -0
  41. doit_cli/prompts/interactive.py +390 -0
  42. doit_cli/rules/__init__.py +5 -0
  43. doit_cli/rules/builtin_rules.py +160 -0
  44. doit_cli/services/__init__.py +79 -0
  45. doit_cli/services/agent_detector.py +168 -0
  46. doit_cli/services/analytics_service.py +218 -0
  47. doit_cli/services/architecture_generator.py +290 -0
  48. doit_cli/services/backup_service.py +204 -0
  49. doit_cli/services/config_loader.py +113 -0
  50. doit_cli/services/context_loader.py +1123 -0
  51. doit_cli/services/coverage_calculator.py +142 -0
  52. doit_cli/services/crossref_service.py +237 -0
  53. doit_cli/services/cycle_time_calculator.py +134 -0
  54. doit_cli/services/date_inferrer.py +349 -0
  55. doit_cli/services/diagram_service.py +337 -0
  56. doit_cli/services/drift_detector.py +109 -0
  57. doit_cli/services/entity_parser.py +301 -0
  58. doit_cli/services/er_diagram_generator.py +197 -0
  59. doit_cli/services/fixit_service.py +699 -0
  60. doit_cli/services/github_service.py +192 -0
  61. doit_cli/services/hook_manager.py +258 -0
  62. doit_cli/services/hook_validator.py +528 -0
  63. doit_cli/services/input_validator.py +322 -0
  64. doit_cli/services/memory_search.py +527 -0
  65. doit_cli/services/mermaid_validator.py +334 -0
  66. doit_cli/services/prompt_transformer.py +91 -0
  67. doit_cli/services/prompt_writer.py +133 -0
  68. doit_cli/services/query_interpreter.py +428 -0
  69. doit_cli/services/report_exporter.py +219 -0
  70. doit_cli/services/report_generator.py +256 -0
  71. doit_cli/services/requirement_parser.py +112 -0
  72. doit_cli/services/roadmap_summarizer.py +209 -0
  73. doit_cli/services/rule_engine.py +443 -0
  74. doit_cli/services/scaffolder.py +215 -0
  75. doit_cli/services/score_calculator.py +172 -0
  76. doit_cli/services/section_parser.py +204 -0
  77. doit_cli/services/spec_scanner.py +327 -0
  78. doit_cli/services/state_manager.py +355 -0
  79. doit_cli/services/status_reporter.py +143 -0
  80. doit_cli/services/task_parser.py +347 -0
  81. doit_cli/services/template_manager.py +710 -0
  82. doit_cli/services/template_reader.py +158 -0
  83. doit_cli/services/user_journey_generator.py +214 -0
  84. doit_cli/services/user_story_parser.py +232 -0
  85. doit_cli/services/validation_service.py +188 -0
  86. doit_cli/services/validator.py +232 -0
  87. doit_cli/services/velocity_tracker.py +173 -0
  88. doit_cli/services/workflow_engine.py +405 -0
  89. doit_cli/templates/agent-file-template.md +28 -0
  90. doit_cli/templates/checklist-template.md +39 -0
  91. doit_cli/templates/commands/doit.checkin.md +363 -0
  92. doit_cli/templates/commands/doit.constitution.md +187 -0
  93. doit_cli/templates/commands/doit.documentit.md +485 -0
  94. doit_cli/templates/commands/doit.fixit.md +181 -0
  95. doit_cli/templates/commands/doit.implementit.md +265 -0
  96. doit_cli/templates/commands/doit.planit.md +262 -0
  97. doit_cli/templates/commands/doit.reviewit.md +355 -0
  98. doit_cli/templates/commands/doit.roadmapit.md +389 -0
  99. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  100. doit_cli/templates/commands/doit.specit.md +521 -0
  101. doit_cli/templates/commands/doit.taskit.md +304 -0
  102. doit_cli/templates/commands/doit.testit.md +277 -0
  103. doit_cli/templates/config/context.yaml +134 -0
  104. doit_cli/templates/config/hooks.yaml +93 -0
  105. doit_cli/templates/config/validation-rules.yaml +64 -0
  106. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  107. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  108. doit_cli/templates/github-issue-templates/task.yml +129 -0
  109. doit_cli/templates/hooks/.gitkeep +0 -0
  110. doit_cli/templates/hooks/post-commit.sh +25 -0
  111. doit_cli/templates/hooks/post-merge.sh +75 -0
  112. doit_cli/templates/hooks/pre-commit.sh +17 -0
  113. doit_cli/templates/hooks/pre-push.sh +18 -0
  114. doit_cli/templates/memory/completed_roadmap.md +50 -0
  115. doit_cli/templates/memory/constitution.md +125 -0
  116. doit_cli/templates/memory/roadmap.md +61 -0
  117. doit_cli/templates/plan-template.md +146 -0
  118. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  119. doit_cli/templates/scripts/bash/common.sh +156 -0
  120. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  121. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  122. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  123. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  124. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  125. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  126. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  127. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  128. doit_cli/templates/spec-template.md +159 -0
  129. doit_cli/templates/tasks-template.md +313 -0
  130. doit_cli/templates/vscode-settings.json +14 -0
  131. doit_toolkit_cli-0.1.10.dist-info/METADATA +324 -0
  132. doit_toolkit_cli-0.1.10.dist-info/RECORD +135 -0
  133. doit_toolkit_cli-0.1.10.dist-info/WHEEL +4 -0
  134. doit_toolkit_cli-0.1.10.dist-info/entry_points.txt +2 -0
  135. 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