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,710 @@
1
+ """Template manager service for copying bundled templates."""
2
+
3
+ import importlib.resources
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ import shutil
7
+
8
+ from ..models.agent import Agent
9
+ from ..models.template import Template, DOIT_COMMANDS
10
+ from ..models.sync_models import CommandTemplate
11
+ from .prompt_transformer import PromptTransformer
12
+
13
+
14
+ # Workflow templates to copy to .doit/templates/
15
+ WORKFLOW_TEMPLATES = [
16
+ "spec-template.md",
17
+ "plan-template.md",
18
+ "tasks-template.md",
19
+ "checklist-template.md",
20
+ ]
21
+
22
+ # GitHub issue templates to copy to .github/ISSUE_TEMPLATE/
23
+ GITHUB_ISSUE_TEMPLATES = [
24
+ "epic.yml",
25
+ "feature.yml",
26
+ "task.yml",
27
+ ]
28
+
29
+ # Workflow scripts to copy to .doit/scripts/bash/
30
+ WORKFLOW_SCRIPTS = [
31
+ "common.sh",
32
+ "check-prerequisites.sh",
33
+ "create-new-feature.sh",
34
+ "setup-plan.sh",
35
+ "update-agent-context.sh",
36
+ ]
37
+
38
+ # Memory templates to copy to .doit/memory/
39
+ MEMORY_TEMPLATES = [
40
+ "constitution.md",
41
+ "roadmap.md",
42
+ "completed_roadmap.md",
43
+ ]
44
+
45
+ # Config templates to copy to .doit/config/
46
+ CONFIG_TEMPLATES = [
47
+ "context.yaml",
48
+ ]
49
+
50
+
51
+ class TemplateManager:
52
+ """Service for managing and copying bundled templates."""
53
+
54
+ # Copilot instructions section markers
55
+ COPILOT_SECTION_START = "<!-- DOIT INSTRUCTIONS START -->"
56
+ COPILOT_SECTION_END = "<!-- DOIT INSTRUCTIONS END -->"
57
+
58
+ def __init__(self, custom_source: Optional[Path] = None):
59
+ """Initialize template manager.
60
+
61
+ Args:
62
+ custom_source: Optional custom template source directory.
63
+ If None, uses bundled templates.
64
+ """
65
+ self.custom_source = custom_source
66
+
67
+ def get_base_template_path(self) -> Path:
68
+ """Get the base path for all templates.
69
+
70
+ Returns:
71
+ Path to base templates directory
72
+ """
73
+ if self.custom_source:
74
+ return self.custom_source
75
+
76
+ # Use bundled templates
77
+ try:
78
+ # Try importlib.resources first (Python 3.9+)
79
+ import importlib.resources as resources
80
+
81
+ # Get the package location
82
+ with resources.as_file(resources.files("doit_cli")) as pkg_path:
83
+ return pkg_path / "templates"
84
+ except (ImportError, TypeError, AttributeError):
85
+ # Fallback: look relative to this file
86
+ # During development, templates are at repo_root/templates/
87
+ module_path = Path(__file__).parent.parent.parent.parent
88
+ return module_path / "templates"
89
+
90
+ def get_template_source_path(self, agent: Agent) -> Path:
91
+ """Get the source path for templates.
92
+
93
+ Args:
94
+ agent: Target agent
95
+
96
+ Returns:
97
+ Path to template directory
98
+ """
99
+ if self.custom_source:
100
+ return self.custom_source / agent.template_directory
101
+
102
+ return self.get_base_template_path() / agent.template_directory
103
+
104
+ def get_bundled_templates(self, agent: Agent) -> list[Template]:
105
+ """Get all bundled templates for an agent.
106
+
107
+ Args:
108
+ agent: Target agent
109
+
110
+ Returns:
111
+ List of Template objects
112
+ """
113
+ source_dir = self.get_template_source_path(agent)
114
+
115
+ if not source_dir.exists():
116
+ return []
117
+
118
+ templates = []
119
+ for file_path in source_dir.iterdir():
120
+ if file_path.is_file() and file_path.suffix == ".md":
121
+ try:
122
+ template = Template.from_file(file_path, agent)
123
+ templates.append(template)
124
+ except Exception:
125
+ # Skip files that can't be parsed
126
+ continue
127
+
128
+ return templates
129
+
130
+ def validate_template_source(self, agent: Agent) -> dict:
131
+ """Validate template source has all required templates.
132
+
133
+ Args:
134
+ agent: Target agent
135
+
136
+ Returns:
137
+ Dict with 'valid' bool, 'found' list, 'missing' list, 'extra' list,
138
+ and 'source_exists' bool
139
+ """
140
+ source_path = self.get_template_source_path(agent)
141
+ source_exists = source_path.exists()
142
+
143
+ if not source_exists:
144
+ return {
145
+ "valid": False,
146
+ "found": [],
147
+ "missing": list(DOIT_COMMANDS),
148
+ "extra": [],
149
+ "source_exists": False,
150
+ "source_path": str(source_path),
151
+ }
152
+
153
+ templates = self.get_bundled_templates(agent)
154
+ found_names = {t.name for t in templates}
155
+ required = set(DOIT_COMMANDS)
156
+
157
+ missing = required - found_names
158
+ extra = found_names - required
159
+
160
+ return {
161
+ "valid": len(missing) == 0,
162
+ "found": list(found_names),
163
+ "missing": list(missing),
164
+ "extra": list(extra),
165
+ "source_exists": True,
166
+ "source_path": str(source_path),
167
+ }
168
+
169
+ def validate_custom_source(self) -> dict:
170
+ """Validate custom template source directory.
171
+
172
+ Returns:
173
+ Dict with validation results for all agents
174
+ """
175
+ if not self.custom_source:
176
+ return {"valid": True, "is_custom": False}
177
+
178
+ if not self.custom_source.exists():
179
+ return {
180
+ "valid": False,
181
+ "is_custom": True,
182
+ "error": f"Custom template directory does not exist: {self.custom_source}",
183
+ }
184
+
185
+ if not self.custom_source.is_dir():
186
+ return {
187
+ "valid": False,
188
+ "is_custom": True,
189
+ "error": f"Custom template path is not a directory: {self.custom_source}",
190
+ }
191
+
192
+ # Check for at least one agent's templates
193
+ claude_result = self.validate_template_source(Agent.CLAUDE)
194
+ copilot_result = self.validate_template_source(Agent.COPILOT)
195
+
196
+ has_any_templates = (
197
+ claude_result.get("source_exists", False) or
198
+ copilot_result.get("source_exists", False)
199
+ )
200
+
201
+ return {
202
+ "valid": has_any_templates,
203
+ "is_custom": True,
204
+ "claude": claude_result,
205
+ "copilot": copilot_result,
206
+ "warnings": self._get_validation_warnings(claude_result, copilot_result),
207
+ }
208
+
209
+ def _get_validation_warnings(self, claude_result: dict, copilot_result: dict) -> list[str]:
210
+ """Generate warning messages from validation results.
211
+
212
+ Args:
213
+ claude_result: Validation result for Claude
214
+ copilot_result: Validation result for Copilot
215
+
216
+ Returns:
217
+ List of warning messages
218
+ """
219
+ warnings = []
220
+
221
+ if not claude_result.get("source_exists", False):
222
+ warnings.append(
223
+ f"No Claude templates found at: {claude_result.get('source_path', 'unknown')}"
224
+ )
225
+ elif claude_result.get("missing"):
226
+ warnings.append(
227
+ f"Missing Claude templates: {', '.join(sorted(claude_result['missing']))}"
228
+ )
229
+
230
+ if not copilot_result.get("source_exists", False):
231
+ warnings.append(
232
+ f"No Copilot templates found at: {copilot_result.get('source_path', 'unknown')}"
233
+ )
234
+ elif copilot_result.get("missing"):
235
+ warnings.append(
236
+ f"Missing Copilot templates: {', '.join(sorted(copilot_result['missing']))}"
237
+ )
238
+
239
+ return warnings
240
+
241
+ def _get_command_templates(self) -> list[Template]:
242
+ """Get all command templates from the unified source directory.
243
+
244
+ Always reads from commands/ directory and parses as Claude format,
245
+ since that's the canonical source for all agents.
246
+
247
+ Returns:
248
+ List of Template objects with names extracted from source files.
249
+ """
250
+ source_dir = self.get_base_template_path() / "commands"
251
+
252
+ if not source_dir.exists():
253
+ return []
254
+
255
+ templates = []
256
+ for file_path in source_dir.iterdir():
257
+ if file_path.is_file() and file_path.suffix == ".md":
258
+ try:
259
+ # Always parse as Claude format since source is commands/
260
+ template = Template.from_file(file_path, Agent.CLAUDE)
261
+ templates.append(template)
262
+ except Exception:
263
+ # Skip files that can't be parsed
264
+ continue
265
+
266
+ return templates
267
+
268
+ def _transform_and_write_templates(
269
+ self,
270
+ templates: list[Template],
271
+ target_dir: Path,
272
+ overwrite: bool = False,
273
+ ) -> dict:
274
+ """Transform command templates to Copilot prompt format and write them.
275
+
276
+ Args:
277
+ templates: List of source templates (in Claude/command format)
278
+ target_dir: Destination directory
279
+ overwrite: Whether to overwrite existing files
280
+
281
+ Returns:
282
+ Dict with 'created', 'updated', 'skipped' lists of paths
283
+ """
284
+ result = {
285
+ "created": [],
286
+ "updated": [],
287
+ "skipped": [],
288
+ }
289
+
290
+ transformer = PromptTransformer()
291
+
292
+ for template in templates:
293
+ # Create CommandTemplate for the transformer
294
+ command_template = CommandTemplate.from_path(template.source_path)
295
+
296
+ # Transform the content
297
+ transformed_content = transformer.transform(command_template)
298
+
299
+ # Generate Copilot filename: doit.{name}.prompt.md
300
+ target_filename = f"doit.{template.name}.prompt.md"
301
+ target_path = target_dir / target_filename
302
+
303
+ if target_path.exists():
304
+ if overwrite:
305
+ target_path.write_text(transformed_content, encoding="utf-8")
306
+ result["updated"].append(target_path)
307
+ else:
308
+ result["skipped"].append(target_path)
309
+ else:
310
+ target_path.write_text(transformed_content, encoding="utf-8")
311
+ result["created"].append(target_path)
312
+
313
+ return result
314
+
315
+ def copy_templates_for_agent(
316
+ self,
317
+ agent: Agent,
318
+ target_dir: Path,
319
+ overwrite: bool = False,
320
+ ) -> dict:
321
+ """Copy templates for an agent to target directory.
322
+
323
+ For Claude: Direct copy from commands/ directory.
324
+ For Copilot: Transform command templates to prompt format.
325
+
326
+ Args:
327
+ agent: Target agent
328
+ target_dir: Destination directory
329
+ overwrite: Whether to overwrite existing files
330
+
331
+ Returns:
332
+ Dict with 'created', 'updated', 'skipped' lists of paths
333
+ """
334
+ # Get templates from unified source (commands/)
335
+ templates = self._get_command_templates()
336
+
337
+ if agent.needs_transformation:
338
+ # Copilot: Transform command templates to prompt format
339
+ return self._transform_and_write_templates(
340
+ templates, target_dir, overwrite
341
+ )
342
+
343
+ # Claude: Direct copy with correct filename
344
+ result = {
345
+ "created": [],
346
+ "updated": [],
347
+ "skipped": [],
348
+ }
349
+
350
+ for template in templates:
351
+ target_path = target_dir / template.target_filename
352
+
353
+ if target_path.exists():
354
+ if overwrite:
355
+ # Overwrite existing file
356
+ target_path.write_text(template.content, encoding="utf-8")
357
+ result["updated"].append(target_path)
358
+ else:
359
+ # Skip existing file
360
+ result["skipped"].append(target_path)
361
+ else:
362
+ # Create new file
363
+ target_path.write_text(template.content, encoding="utf-8")
364
+ result["created"].append(target_path)
365
+
366
+ return result
367
+
368
+ def copy_single_template(
369
+ self,
370
+ agent: Agent,
371
+ template_name: str,
372
+ target_dir: Path,
373
+ overwrite: bool = False,
374
+ ) -> Optional[Path]:
375
+ """Copy a single template.
376
+
377
+ Args:
378
+ agent: Target agent
379
+ template_name: Name of template (e.g., 'specit')
380
+ target_dir: Destination directory
381
+ overwrite: Whether to overwrite existing file
382
+
383
+ Returns:
384
+ Path to created/updated file, or None if skipped
385
+ """
386
+ templates = self.get_bundled_templates(agent)
387
+ template = next((t for t in templates if t.name == template_name), None)
388
+
389
+ if template is None:
390
+ return None
391
+
392
+ target_path = target_dir / template.target_filename
393
+
394
+ if target_path.exists() and not overwrite:
395
+ return None
396
+
397
+ target_path.write_text(template.content, encoding="utf-8")
398
+ return target_path
399
+
400
+ def create_copilot_instructions(
401
+ self,
402
+ target_path: Path,
403
+ update_only: bool = False,
404
+ ) -> bool:
405
+ """Create or update .github/copilot-instructions.md with doit section.
406
+
407
+ Args:
408
+ target_path: Path to copilot-instructions.md
409
+ update_only: If True, only update existing file
410
+
411
+ Returns:
412
+ True if file was created/updated
413
+ """
414
+ doit_section = f"""{self.COPILOT_SECTION_START}
415
+ ## Doit Workflow Commands
416
+
417
+ This project uses the Doit workflow for structured development. The following prompts are available in `.github/prompts/`:
418
+
419
+ | Command | Description |
420
+ |---------|-------------|
421
+ | #doit-specit | Create feature specifications |
422
+ | #doit-planit | Generate implementation plans |
423
+ | #doit-taskit | Create task breakdowns |
424
+ | #doit-implementit | Execute implementation tasks |
425
+ | #doit-testit | Run tests and generate reports |
426
+ | #doit-reviewit | Review code for quality |
427
+ | #doit-checkin | Complete feature and create PR |
428
+ | #doit-constitution | Manage project constitution |
429
+ | #doit-scaffoldit | Scaffold new projects |
430
+ | #doit-roadmapit | Manage feature roadmap |
431
+ | #doit-documentit | Manage documentation |
432
+
433
+ Use the agent mode (`@workspace /doit-*`) for multi-step workflows.
434
+ {self.COPILOT_SECTION_END}"""
435
+
436
+ if target_path.exists():
437
+ content = target_path.read_text(encoding="utf-8")
438
+
439
+ # Check if doit section already exists
440
+ if self.COPILOT_SECTION_START in content:
441
+ # Replace existing section
442
+ start_idx = content.find(self.COPILOT_SECTION_START)
443
+ end_idx = content.find(self.COPILOT_SECTION_END)
444
+ if end_idx != -1:
445
+ end_idx += len(self.COPILOT_SECTION_END)
446
+ new_content = content[:start_idx] + doit_section + content[end_idx:]
447
+ target_path.write_text(new_content, encoding="utf-8")
448
+ return True
449
+ else:
450
+ # Append doit section
451
+ new_content = content.rstrip() + "\n\n" + doit_section + "\n"
452
+ target_path.write_text(new_content, encoding="utf-8")
453
+ return True
454
+ elif not update_only:
455
+ # Create new file
456
+ content = f"# Copilot Instructions\n\n{doit_section}\n"
457
+ target_path.parent.mkdir(parents=True, exist_ok=True)
458
+ target_path.write_text(content, encoding="utf-8")
459
+ return True
460
+
461
+ return False
462
+
463
+ def copy_workflow_templates(
464
+ self,
465
+ target_dir: Path,
466
+ overwrite: bool = False,
467
+ ) -> dict:
468
+ """Copy workflow templates (spec, plan, tasks, checklist) to target directory.
469
+
470
+ These are the templates used by doit commands to generate artifacts.
471
+
472
+ Args:
473
+ target_dir: Destination directory (typically .doit/templates/)
474
+ overwrite: Whether to overwrite existing files
475
+
476
+ Returns:
477
+ Dict with 'created', 'updated', 'skipped' lists of paths
478
+ """
479
+ result = {
480
+ "created": [],
481
+ "updated": [],
482
+ "skipped": [],
483
+ }
484
+
485
+ source_dir = self.get_base_template_path()
486
+ if not source_dir.exists():
487
+ return result
488
+
489
+ # Ensure target directory exists
490
+ target_dir.mkdir(parents=True, exist_ok=True)
491
+
492
+ for template_name in WORKFLOW_TEMPLATES:
493
+ source_path = source_dir / template_name
494
+ if not source_path.exists():
495
+ continue
496
+
497
+ target_path = target_dir / template_name
498
+
499
+ if target_path.exists():
500
+ if overwrite:
501
+ shutil.copy2(source_path, target_path)
502
+ result["updated"].append(target_path)
503
+ else:
504
+ result["skipped"].append(target_path)
505
+ else:
506
+ shutil.copy2(source_path, target_path)
507
+ result["created"].append(target_path)
508
+
509
+ return result
510
+
511
+ def copy_github_issue_templates(
512
+ self,
513
+ target_dir: Path,
514
+ overwrite: bool = False,
515
+ ) -> dict:
516
+ """Copy GitHub issue templates (epic, feature, task) to target directory.
517
+
518
+ These are YAML templates for GitHub Issues.
519
+
520
+ Args:
521
+ target_dir: Destination directory (typically .github/ISSUE_TEMPLATE/)
522
+ overwrite: Whether to overwrite existing files
523
+
524
+ Returns:
525
+ Dict with 'created', 'updated', 'skipped' lists of paths
526
+ """
527
+ result = {
528
+ "created": [],
529
+ "updated": [],
530
+ "skipped": [],
531
+ }
532
+
533
+ # GitHub issue templates are stored in a different location
534
+ # They're at the repo root .github/ISSUE_TEMPLATE/, not in templates/
535
+ # But for bundled distribution, we'll look in templates/github-issue-templates/
536
+ source_dir = self.get_base_template_path() / "github-issue-templates"
537
+
538
+ if not source_dir.exists():
539
+ # Fallback: try repo root (development mode)
540
+ module_path = Path(__file__).parent.parent.parent.parent
541
+ source_dir = module_path / ".github" / "ISSUE_TEMPLATE"
542
+
543
+ if not source_dir.exists():
544
+ return result
545
+
546
+ # Ensure target directory exists
547
+ target_dir.mkdir(parents=True, exist_ok=True)
548
+
549
+ for template_name in GITHUB_ISSUE_TEMPLATES:
550
+ source_path = source_dir / template_name
551
+ if not source_path.exists():
552
+ continue
553
+
554
+ target_path = target_dir / template_name
555
+
556
+ if target_path.exists():
557
+ if overwrite:
558
+ shutil.copy2(source_path, target_path)
559
+ result["updated"].append(target_path)
560
+ else:
561
+ result["skipped"].append(target_path)
562
+ else:
563
+ shutil.copy2(source_path, target_path)
564
+ result["created"].append(target_path)
565
+
566
+ return result
567
+
568
+ def copy_scripts(
569
+ self,
570
+ target_dir: Path,
571
+ overwrite: bool = False,
572
+ ) -> dict:
573
+ """Copy workflow scripts to target directory.
574
+
575
+ These are bash scripts used by doit commands for workflow automation.
576
+
577
+ Args:
578
+ target_dir: Destination directory (typically .doit/scripts/bash/)
579
+ overwrite: Whether to overwrite existing files
580
+
581
+ Returns:
582
+ Dict with 'created', 'updated', 'skipped' lists of paths
583
+ """
584
+ result = {
585
+ "created": [],
586
+ "updated": [],
587
+ "skipped": [],
588
+ }
589
+
590
+ source_dir = self.get_base_template_path() / "scripts" / "bash"
591
+ if not source_dir.exists():
592
+ return result
593
+
594
+ # Ensure target directory exists
595
+ target_dir.mkdir(parents=True, exist_ok=True)
596
+
597
+ for script_name in WORKFLOW_SCRIPTS:
598
+ source_path = source_dir / script_name
599
+ if not source_path.exists():
600
+ continue
601
+
602
+ target_path = target_dir / script_name
603
+
604
+ if target_path.exists():
605
+ if overwrite:
606
+ shutil.copy2(source_path, target_path)
607
+ result["updated"].append(target_path)
608
+ else:
609
+ result["skipped"].append(target_path)
610
+ else:
611
+ shutil.copy2(source_path, target_path)
612
+ result["created"].append(target_path)
613
+
614
+ return result
615
+
616
+ def copy_memory_templates(
617
+ self,
618
+ target_dir: Path,
619
+ overwrite: bool = False,
620
+ ) -> dict:
621
+ """Copy memory templates (constitution, roadmap, roadmap_completed) to target directory.
622
+
623
+ These are the starter templates for project memory used by doit commands.
624
+
625
+ Args:
626
+ target_dir: Destination directory (typically .doit/memory/)
627
+ overwrite: Whether to overwrite existing files
628
+
629
+ Returns:
630
+ Dict with 'created', 'updated', 'skipped' lists of paths
631
+ """
632
+ result = {
633
+ "created": [],
634
+ "updated": [],
635
+ "skipped": [],
636
+ }
637
+
638
+ source_dir = self.get_base_template_path() / "memory"
639
+ if not source_dir.exists():
640
+ return result
641
+
642
+ # Ensure target directory exists
643
+ target_dir.mkdir(parents=True, exist_ok=True)
644
+
645
+ for template_name in MEMORY_TEMPLATES:
646
+ source_path = source_dir / template_name
647
+ if not source_path.exists():
648
+ continue
649
+
650
+ target_path = target_dir / template_name
651
+
652
+ if target_path.exists():
653
+ if overwrite:
654
+ shutil.copy2(source_path, target_path)
655
+ result["updated"].append(target_path)
656
+ else:
657
+ result["skipped"].append(target_path)
658
+ else:
659
+ shutil.copy2(source_path, target_path)
660
+ result["created"].append(target_path)
661
+
662
+ return result
663
+
664
+ def copy_config_templates(
665
+ self,
666
+ target_dir: Path,
667
+ overwrite: bool = False,
668
+ ) -> dict:
669
+ """Copy config templates (context.yaml) to target directory.
670
+
671
+ These are configuration file templates for customizing doit behavior.
672
+
673
+ Args:
674
+ target_dir: Destination directory (typically .doit/config/)
675
+ overwrite: Whether to overwrite existing files
676
+
677
+ Returns:
678
+ Dict with 'created', 'updated', 'skipped' lists of paths
679
+ """
680
+ result = {
681
+ "created": [],
682
+ "updated": [],
683
+ "skipped": [],
684
+ }
685
+
686
+ source_dir = self.get_base_template_path() / "config"
687
+ if not source_dir.exists():
688
+ return result
689
+
690
+ # Ensure target directory exists
691
+ target_dir.mkdir(parents=True, exist_ok=True)
692
+
693
+ for template_name in CONFIG_TEMPLATES:
694
+ source_path = source_dir / template_name
695
+ if not source_path.exists():
696
+ continue
697
+
698
+ target_path = target_dir / template_name
699
+
700
+ if target_path.exists():
701
+ if overwrite:
702
+ shutil.copy2(source_path, target_path)
703
+ result["updated"].append(target_path)
704
+ else:
705
+ result["skipped"].append(target_path)
706
+ else:
707
+ shutil.copy2(source_path, target_path)
708
+ result["created"].append(target_path)
709
+
710
+ return result