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,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
|