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,613 @@
|
|
|
1
|
+
"""Init command for initializing doit project structure."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.tree import Tree
|
|
11
|
+
|
|
12
|
+
from ..models.agent import Agent
|
|
13
|
+
from ..models.project import Project
|
|
14
|
+
from ..models.results import InitResult
|
|
15
|
+
from ..models.workflow_models import Workflow, WorkflowStep
|
|
16
|
+
from ..services.scaffolder import Scaffolder
|
|
17
|
+
from ..services.state_manager import StateManager
|
|
18
|
+
from ..services.workflow_engine import WorkflowEngine
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
# Type aliases for CLI options
|
|
24
|
+
AgentOption = Annotated[
|
|
25
|
+
Optional[str],
|
|
26
|
+
typer.Option(
|
|
27
|
+
"--agent", "-a",
|
|
28
|
+
help="Target agent(s): claude, copilot, or claude,copilot for both"
|
|
29
|
+
)
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
TemplatesOption = Annotated[
|
|
33
|
+
Optional[Path],
|
|
34
|
+
typer.Option(
|
|
35
|
+
"--templates", "-t",
|
|
36
|
+
help="Custom template directory path"
|
|
37
|
+
)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
UpdateFlag = Annotated[
|
|
41
|
+
bool,
|
|
42
|
+
typer.Option(
|
|
43
|
+
"--update", "-u",
|
|
44
|
+
help="Update existing project, preserving custom files"
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
ForceFlag = Annotated[
|
|
49
|
+
bool,
|
|
50
|
+
typer.Option(
|
|
51
|
+
"--force", "-f",
|
|
52
|
+
help="Overwrite existing files without backup"
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
YesFlag = Annotated[
|
|
57
|
+
bool,
|
|
58
|
+
typer.Option(
|
|
59
|
+
"--yes", "-y",
|
|
60
|
+
help="Skip confirmation prompts"
|
|
61
|
+
)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# InitWorkflow Factory (Feature 031)
|
|
67
|
+
# =============================================================================
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_init_workflow(path: Path) -> Workflow:
|
|
71
|
+
"""Create the init workflow definition.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
path: Target project directory path
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Workflow instance configured for init command
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
workflow = create_init_workflow(Path("."))
|
|
81
|
+
responses = engine.run(workflow)
|
|
82
|
+
"""
|
|
83
|
+
return Workflow(
|
|
84
|
+
id="init-workflow",
|
|
85
|
+
command_name="init",
|
|
86
|
+
description="Initialize a new doit project",
|
|
87
|
+
interactive=True,
|
|
88
|
+
steps=[
|
|
89
|
+
WorkflowStep(
|
|
90
|
+
id="select-agent",
|
|
91
|
+
name="Select AI Agent",
|
|
92
|
+
prompt_text="Which AI agent(s) do you want to initialize for?",
|
|
93
|
+
required=True,
|
|
94
|
+
order=0,
|
|
95
|
+
validation_type="ChoiceValidator",
|
|
96
|
+
default_value="claude",
|
|
97
|
+
options={
|
|
98
|
+
"claude": "Claude Code",
|
|
99
|
+
"copilot": "GitHub Copilot",
|
|
100
|
+
"both": "Both agents",
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
WorkflowStep(
|
|
104
|
+
id="confirm-path",
|
|
105
|
+
name="Confirm Project Path",
|
|
106
|
+
prompt_text=f"Initialize doit in '{path}'?",
|
|
107
|
+
required=True,
|
|
108
|
+
order=1,
|
|
109
|
+
validation_type=None,
|
|
110
|
+
default_value="yes",
|
|
111
|
+
options={"yes": "Confirm", "no": "Cancel"},
|
|
112
|
+
),
|
|
113
|
+
WorkflowStep(
|
|
114
|
+
id="custom-templates",
|
|
115
|
+
name="Custom Templates",
|
|
116
|
+
prompt_text="Custom template directory (leave empty for default)",
|
|
117
|
+
required=False,
|
|
118
|
+
order=2,
|
|
119
|
+
validation_type="PathExistsValidator",
|
|
120
|
+
default_value="",
|
|
121
|
+
),
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def map_workflow_responses(responses: dict) -> tuple[list[Agent], Optional[Path]]:
|
|
127
|
+
"""Map workflow responses to init parameters.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
responses: Dict from WorkflowEngine.run()
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Tuple of (agents list, template_source path or None)
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
typer.Exit: If confirm-path is "no"
|
|
137
|
+
"""
|
|
138
|
+
# Check confirmation
|
|
139
|
+
if responses.get("confirm-path") == "no":
|
|
140
|
+
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
141
|
+
raise typer.Exit(0)
|
|
142
|
+
|
|
143
|
+
# Parse agent selection
|
|
144
|
+
agent_str = responses.get("select-agent", "claude")
|
|
145
|
+
if agent_str == "both":
|
|
146
|
+
agents = [Agent.CLAUDE, Agent.COPILOT]
|
|
147
|
+
elif agent_str == "copilot":
|
|
148
|
+
agents = [Agent.COPILOT]
|
|
149
|
+
else:
|
|
150
|
+
agents = [Agent.CLAUDE]
|
|
151
|
+
|
|
152
|
+
# Parse template path
|
|
153
|
+
template_str = responses.get("custom-templates", "")
|
|
154
|
+
template_source = Path(template_str) if template_str else None
|
|
155
|
+
|
|
156
|
+
return agents, template_source
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def display_init_result(result: InitResult, agents: list[Agent]) -> None:
|
|
160
|
+
"""Display initialization result with rich formatting.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
result: The initialization result to display
|
|
164
|
+
agents: List of agents that were initialized
|
|
165
|
+
"""
|
|
166
|
+
if not result.success:
|
|
167
|
+
console.print(
|
|
168
|
+
Panel(
|
|
169
|
+
f"[red]Error:[/red] {result.error_message}",
|
|
170
|
+
title="[red]Initialization Failed[/red]",
|
|
171
|
+
border_style="red",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# Create a tree view of created structure
|
|
177
|
+
tree = Tree("[bold cyan]Doit Project Structure[/bold cyan]")
|
|
178
|
+
|
|
179
|
+
if result.created_directories:
|
|
180
|
+
dirs_branch = tree.add("[green]Created Directories[/green]")
|
|
181
|
+
for dir_path in sorted(result.created_directories):
|
|
182
|
+
rel_path = dir_path.relative_to(result.project.path)
|
|
183
|
+
dirs_branch.add(f"[dim]{rel_path}/[/dim]")
|
|
184
|
+
|
|
185
|
+
if result.created_files:
|
|
186
|
+
files_branch = tree.add("[green]Created Files[/green]")
|
|
187
|
+
for file_path in sorted(result.created_files):
|
|
188
|
+
rel_path = file_path.relative_to(result.project.path)
|
|
189
|
+
files_branch.add(f"[dim]{rel_path}[/dim]")
|
|
190
|
+
|
|
191
|
+
if result.updated_files:
|
|
192
|
+
updated_branch = tree.add("[yellow]Updated Files[/yellow]")
|
|
193
|
+
for file_path in sorted(result.updated_files):
|
|
194
|
+
rel_path = file_path.relative_to(result.project.path)
|
|
195
|
+
updated_branch.add(f"[dim]{rel_path}[/dim]")
|
|
196
|
+
|
|
197
|
+
if result.skipped_files:
|
|
198
|
+
skipped_branch = tree.add("[dim]Skipped Files (already exist)[/dim]")
|
|
199
|
+
for file_path in sorted(result.skipped_files):
|
|
200
|
+
rel_path = file_path.relative_to(result.project.path)
|
|
201
|
+
skipped_branch.add(f"[dim]{rel_path}[/dim]")
|
|
202
|
+
|
|
203
|
+
console.print()
|
|
204
|
+
console.print(Panel(tree, title="[bold green]Initialization Complete[/bold green]", border_style="green"))
|
|
205
|
+
|
|
206
|
+
# Display summary
|
|
207
|
+
console.print()
|
|
208
|
+
console.print(f"[bold]Summary:[/bold] {result.summary}")
|
|
209
|
+
|
|
210
|
+
# Display next steps
|
|
211
|
+
display_next_steps(agents)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def display_next_steps(agents: list[Agent]) -> None:
|
|
215
|
+
"""Display next steps guidance after initialization.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
agents: List of agents that were initialized
|
|
219
|
+
"""
|
|
220
|
+
steps = [
|
|
221
|
+
"1. Run [cyan]/doit.constitution[/cyan] to establish project principles",
|
|
222
|
+
"2. Run [cyan]/doit.specit[/cyan] to create your first feature specification",
|
|
223
|
+
"3. Run [cyan]/doit.planit[/cyan] to create an implementation plan",
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
agent_names = ", ".join(a.display_name for a in agents)
|
|
227
|
+
|
|
228
|
+
console.print()
|
|
229
|
+
console.print(
|
|
230
|
+
Panel(
|
|
231
|
+
"\n".join(
|
|
232
|
+
[
|
|
233
|
+
f"[bold]Initialized for:[/bold] {agent_names}",
|
|
234
|
+
"",
|
|
235
|
+
"[bold]Next Steps:[/bold]",
|
|
236
|
+
*steps,
|
|
237
|
+
]
|
|
238
|
+
),
|
|
239
|
+
title="[cyan]Getting Started[/cyan]",
|
|
240
|
+
border_style="cyan",
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def prompt_agent_selection() -> list[Agent]:
|
|
246
|
+
"""Prompt user to select target agent(s).
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
List of selected agents
|
|
250
|
+
"""
|
|
251
|
+
console.print()
|
|
252
|
+
console.print("[bold]Select target AI agent(s):[/bold]")
|
|
253
|
+
console.print()
|
|
254
|
+
|
|
255
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
256
|
+
table.add_column("Option", style="cyan", width=8)
|
|
257
|
+
table.add_column("Agent", style="white")
|
|
258
|
+
table.add_column("Command Directory", style="dim")
|
|
259
|
+
|
|
260
|
+
table.add_row("1", "Claude Code", ".claude/commands/")
|
|
261
|
+
table.add_row("2", "GitHub Copilot", ".github/prompts/")
|
|
262
|
+
table.add_row("3", "Both", ".claude/commands/ + .github/prompts/")
|
|
263
|
+
|
|
264
|
+
console.print(table)
|
|
265
|
+
console.print()
|
|
266
|
+
|
|
267
|
+
choice = typer.prompt("Enter your choice (1-3)", default="1")
|
|
268
|
+
|
|
269
|
+
if choice == "1":
|
|
270
|
+
return [Agent.CLAUDE]
|
|
271
|
+
elif choice == "2":
|
|
272
|
+
return [Agent.COPILOT]
|
|
273
|
+
elif choice == "3":
|
|
274
|
+
return [Agent.CLAUDE, Agent.COPILOT]
|
|
275
|
+
else:
|
|
276
|
+
console.print("[yellow]Invalid choice, defaulting to Claude[/yellow]")
|
|
277
|
+
return [Agent.CLAUDE]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def parse_agent_string(agent_str: str) -> list[Agent]:
|
|
281
|
+
"""Parse agent string into list of Agent enums.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
agent_str: Comma-separated agent names (e.g., "claude,copilot")
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
List of Agent enums
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
typer.BadParameter: If invalid agent name provided
|
|
291
|
+
"""
|
|
292
|
+
agents = []
|
|
293
|
+
for name in agent_str.lower().split(","):
|
|
294
|
+
name = name.strip()
|
|
295
|
+
if name == "claude":
|
|
296
|
+
agents.append(Agent.CLAUDE)
|
|
297
|
+
elif name == "copilot":
|
|
298
|
+
agents.append(Agent.COPILOT)
|
|
299
|
+
else:
|
|
300
|
+
raise typer.BadParameter(
|
|
301
|
+
f"Unknown agent: {name}. Use 'claude', 'copilot', or 'claude,copilot'"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return agents
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def validate_custom_templates(template_source: Path, yes: bool = False) -> bool:
|
|
308
|
+
"""Validate custom template source and display warnings.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
template_source: Path to custom template directory
|
|
312
|
+
yes: Skip confirmation prompts
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if should continue, False to abort
|
|
316
|
+
"""
|
|
317
|
+
from ..services.template_manager import TemplateManager
|
|
318
|
+
|
|
319
|
+
template_manager = TemplateManager(template_source)
|
|
320
|
+
validation = template_manager.validate_custom_source()
|
|
321
|
+
|
|
322
|
+
if not validation.get("valid", False):
|
|
323
|
+
error_msg = validation.get("error", "Invalid custom template source")
|
|
324
|
+
console.print(f"[red]Error:[/red] {error_msg}")
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
warnings = validation.get("warnings", [])
|
|
328
|
+
if warnings:
|
|
329
|
+
console.print()
|
|
330
|
+
console.print(
|
|
331
|
+
Panel(
|
|
332
|
+
"\n".join(f"• {w}" for w in warnings),
|
|
333
|
+
title="[yellow]Template Warnings[/yellow]",
|
|
334
|
+
border_style="yellow",
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if not yes:
|
|
339
|
+
if not typer.confirm("Continue with missing templates?", default=True):
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def run_init(
|
|
346
|
+
path: Path,
|
|
347
|
+
agents: Optional[list[Agent]] = None,
|
|
348
|
+
update: bool = False,
|
|
349
|
+
force: bool = False,
|
|
350
|
+
yes: bool = False,
|
|
351
|
+
template_source: Optional[Path] = None,
|
|
352
|
+
) -> InitResult:
|
|
353
|
+
"""Run the initialization process.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
path: Project directory path
|
|
357
|
+
agents: Target agents (None to auto-detect or prompt)
|
|
358
|
+
update: Update existing project
|
|
359
|
+
force: Force overwrite without backup
|
|
360
|
+
yes: Skip confirmation prompts
|
|
361
|
+
template_source: Custom template source path
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
InitResult with operation details
|
|
365
|
+
"""
|
|
366
|
+
# Defer imports to avoid circular dependencies
|
|
367
|
+
from ..services.template_manager import TemplateManager
|
|
368
|
+
from ..services.agent_detector import AgentDetector
|
|
369
|
+
|
|
370
|
+
# Create project model
|
|
371
|
+
project = Project(path=path.resolve())
|
|
372
|
+
|
|
373
|
+
# Validate custom template source if provided
|
|
374
|
+
if template_source:
|
|
375
|
+
if not validate_custom_templates(template_source, yes):
|
|
376
|
+
return InitResult(
|
|
377
|
+
success=False,
|
|
378
|
+
project=project,
|
|
379
|
+
error_message="Custom template validation failed",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Check if safe directory
|
|
383
|
+
if not project.is_safe_directory() and not force:
|
|
384
|
+
if not yes:
|
|
385
|
+
console.print(
|
|
386
|
+
Panel(
|
|
387
|
+
f"[yellow]Warning:[/yellow] You are about to initialize doit in [bold]{path}[/bold]\n\n"
|
|
388
|
+
"This is typically not recommended. Consider initializing in a project directory instead.",
|
|
389
|
+
title="[yellow]Unsafe Directory[/yellow]",
|
|
390
|
+
border_style="yellow",
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
if not typer.confirm("Continue anyway?", default=False):
|
|
394
|
+
return InitResult(
|
|
395
|
+
success=False,
|
|
396
|
+
project=project,
|
|
397
|
+
error_message="Operation cancelled by user",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Determine target agents
|
|
401
|
+
if agents is None:
|
|
402
|
+
detector = AgentDetector(project)
|
|
403
|
+
detected = detector.detect_agents()
|
|
404
|
+
|
|
405
|
+
if detected:
|
|
406
|
+
agents = detected
|
|
407
|
+
agent_names = ", ".join(a.display_name for a in agents)
|
|
408
|
+
console.print(f"[cyan]Auto-detected agent(s):[/cyan] {agent_names}")
|
|
409
|
+
elif yes:
|
|
410
|
+
# Default to Claude if --yes and no detection
|
|
411
|
+
agents = [Agent.CLAUDE]
|
|
412
|
+
console.print("[cyan]Defaulting to Claude Code[/cyan]")
|
|
413
|
+
else:
|
|
414
|
+
agents = prompt_agent_selection()
|
|
415
|
+
|
|
416
|
+
# Create scaffolder and initialize structure
|
|
417
|
+
scaffolder = Scaffolder(project)
|
|
418
|
+
result = scaffolder.create_doit_structure()
|
|
419
|
+
|
|
420
|
+
if not result.success:
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
# Create agent directories and copy templates
|
|
424
|
+
template_manager = TemplateManager(template_source)
|
|
425
|
+
|
|
426
|
+
# Copy workflow templates to .doit/templates/
|
|
427
|
+
workflow_result = template_manager.copy_workflow_templates(
|
|
428
|
+
target_dir=project.doit_folder / "templates",
|
|
429
|
+
overwrite=update or force,
|
|
430
|
+
)
|
|
431
|
+
result.created_files.extend(workflow_result.get("created", []))
|
|
432
|
+
result.updated_files.extend(workflow_result.get("updated", []))
|
|
433
|
+
result.skipped_files.extend(workflow_result.get("skipped", []))
|
|
434
|
+
|
|
435
|
+
# Copy GitHub issue templates to .github/ISSUE_TEMPLATE/
|
|
436
|
+
github_templates_result = template_manager.copy_github_issue_templates(
|
|
437
|
+
target_dir=project.path / ".github" / "ISSUE_TEMPLATE",
|
|
438
|
+
overwrite=update or force,
|
|
439
|
+
)
|
|
440
|
+
result.created_files.extend(github_templates_result.get("created", []))
|
|
441
|
+
result.updated_files.extend(github_templates_result.get("updated", []))
|
|
442
|
+
result.skipped_files.extend(github_templates_result.get("skipped", []))
|
|
443
|
+
|
|
444
|
+
# Copy workflow scripts to .doit/scripts/bash/
|
|
445
|
+
scripts_result = template_manager.copy_scripts(
|
|
446
|
+
target_dir=project.doit_folder / "scripts" / "bash",
|
|
447
|
+
overwrite=update or force,
|
|
448
|
+
)
|
|
449
|
+
result.created_files.extend(scripts_result.get("created", []))
|
|
450
|
+
result.updated_files.extend(scripts_result.get("updated", []))
|
|
451
|
+
result.skipped_files.extend(scripts_result.get("skipped", []))
|
|
452
|
+
|
|
453
|
+
# Copy memory templates to .doit/memory/
|
|
454
|
+
# Note: Memory files (constitution, roadmap) should only be overwritten with --force,
|
|
455
|
+
# not --update, since they contain user-customized project content
|
|
456
|
+
memory_result = template_manager.copy_memory_templates(
|
|
457
|
+
target_dir=project.doit_folder / "memory",
|
|
458
|
+
overwrite=force,
|
|
459
|
+
)
|
|
460
|
+
result.created_files.extend(memory_result.get("created", []))
|
|
461
|
+
result.updated_files.extend(memory_result.get("updated", []))
|
|
462
|
+
result.skipped_files.extend(memory_result.get("skipped", []))
|
|
463
|
+
|
|
464
|
+
# Copy config templates to .doit/config/
|
|
465
|
+
config_result = template_manager.copy_config_templates(
|
|
466
|
+
target_dir=project.doit_folder / "config",
|
|
467
|
+
overwrite=update or force,
|
|
468
|
+
)
|
|
469
|
+
result.created_files.extend(config_result.get("created", []))
|
|
470
|
+
result.updated_files.extend(config_result.get("updated", []))
|
|
471
|
+
result.skipped_files.extend(config_result.get("skipped", []))
|
|
472
|
+
|
|
473
|
+
for agent in agents:
|
|
474
|
+
scaffolder.create_agent_directory(agent)
|
|
475
|
+
|
|
476
|
+
# Copy templates for this agent
|
|
477
|
+
copy_result = template_manager.copy_templates_for_agent(
|
|
478
|
+
agent=agent,
|
|
479
|
+
target_dir=project.command_directory(agent),
|
|
480
|
+
overwrite=update or force,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
result.created_files.extend(copy_result.get("created", []))
|
|
484
|
+
result.updated_files.extend(copy_result.get("updated", []))
|
|
485
|
+
result.skipped_files.extend(copy_result.get("skipped", []))
|
|
486
|
+
|
|
487
|
+
# For Copilot agent, also create/update copilot-instructions.md
|
|
488
|
+
if agent == Agent.COPILOT:
|
|
489
|
+
copilot_instructions_path = project.path / ".github" / "copilot-instructions.md"
|
|
490
|
+
copilot_instructions_path.parent.mkdir(parents=True, exist_ok=True)
|
|
491
|
+
|
|
492
|
+
if template_manager.create_copilot_instructions(
|
|
493
|
+
target_path=copilot_instructions_path,
|
|
494
|
+
update_only=False,
|
|
495
|
+
):
|
|
496
|
+
if copilot_instructions_path in result.created_files:
|
|
497
|
+
pass # Already tracked
|
|
498
|
+
elif copilot_instructions_path.exists():
|
|
499
|
+
result.updated_files.append(copilot_instructions_path)
|
|
500
|
+
else:
|
|
501
|
+
result.created_files.append(copilot_instructions_path)
|
|
502
|
+
|
|
503
|
+
# Update project state
|
|
504
|
+
project.initialized = True
|
|
505
|
+
project.agents = agents
|
|
506
|
+
|
|
507
|
+
return result
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def init_command(
|
|
511
|
+
path: Annotated[
|
|
512
|
+
Path,
|
|
513
|
+
typer.Argument(
|
|
514
|
+
default=...,
|
|
515
|
+
help="Project directory path (use '.' for current directory)"
|
|
516
|
+
)
|
|
517
|
+
] = Path("."),
|
|
518
|
+
agent: AgentOption = None,
|
|
519
|
+
templates: TemplatesOption = None,
|
|
520
|
+
update: UpdateFlag = False,
|
|
521
|
+
force: ForceFlag = False,
|
|
522
|
+
yes: YesFlag = False,
|
|
523
|
+
) -> None:
|
|
524
|
+
"""Initialize a new doit project with bundled templates.
|
|
525
|
+
|
|
526
|
+
This command creates the .doit/ folder structure and copies command
|
|
527
|
+
templates for the specified AI agent(s).
|
|
528
|
+
|
|
529
|
+
Examples:
|
|
530
|
+
doit init . # Initialize in current directory
|
|
531
|
+
doit init . --agent claude # Claude only
|
|
532
|
+
doit init . --agent copilot # Copilot only
|
|
533
|
+
doit init . -a claude,copilot # Both agents
|
|
534
|
+
doit init . --update # Update existing templates
|
|
535
|
+
doit init . --yes # Non-interactive mode
|
|
536
|
+
"""
|
|
537
|
+
# Parse agent string if provided via CLI
|
|
538
|
+
agents = None
|
|
539
|
+
if agent:
|
|
540
|
+
try:
|
|
541
|
+
agents = parse_agent_string(agent)
|
|
542
|
+
except typer.BadParameter as e:
|
|
543
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
544
|
+
raise typer.Exit(1)
|
|
545
|
+
|
|
546
|
+
# Non-interactive mode: bypass workflow entirely (FR-003, FR-008)
|
|
547
|
+
if yes:
|
|
548
|
+
result = run_init(
|
|
549
|
+
path=path,
|
|
550
|
+
agents=agents,
|
|
551
|
+
update=update,
|
|
552
|
+
force=force,
|
|
553
|
+
yes=True,
|
|
554
|
+
template_source=templates,
|
|
555
|
+
)
|
|
556
|
+
display_init_result(result, agents or result.project.agents or [Agent.CLAUDE])
|
|
557
|
+
if not result.success:
|
|
558
|
+
raise typer.Exit(1)
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
# Interactive mode: use workflow engine (FR-001, FR-002, FR-005)
|
|
562
|
+
workflow = create_init_workflow(path)
|
|
563
|
+
|
|
564
|
+
# Fix MT-005: Use target path for state directory, not cwd
|
|
565
|
+
state_dir = path.resolve() / ".doit" / "state"
|
|
566
|
+
engine = WorkflowEngine(
|
|
567
|
+
console=console,
|
|
568
|
+
state_manager=StateManager(state_dir=state_dir),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# Fix MT-007: Pre-populate responses for CLI-provided values
|
|
572
|
+
initial_responses: dict[str, str] = {}
|
|
573
|
+
if agents:
|
|
574
|
+
# Map agents to workflow response value
|
|
575
|
+
if len(agents) == 2:
|
|
576
|
+
initial_responses["select-agent"] = "both"
|
|
577
|
+
elif Agent.COPILOT in agents:
|
|
578
|
+
initial_responses["select-agent"] = "copilot"
|
|
579
|
+
else:
|
|
580
|
+
initial_responses["select-agent"] = "claude"
|
|
581
|
+
if templates:
|
|
582
|
+
initial_responses["custom-templates"] = str(templates)
|
|
583
|
+
|
|
584
|
+
try:
|
|
585
|
+
responses = engine.run(workflow, initial_responses=initial_responses)
|
|
586
|
+
except KeyboardInterrupt:
|
|
587
|
+
# State is saved by workflow engine (FR-007)
|
|
588
|
+
raise typer.Exit(130)
|
|
589
|
+
|
|
590
|
+
# Map workflow responses to init parameters (FR-006)
|
|
591
|
+
workflow_agents, template_source = map_workflow_responses(responses)
|
|
592
|
+
|
|
593
|
+
# Use CLI-provided agents if specified, else use workflow selection
|
|
594
|
+
final_agents = agents if agents else workflow_agents
|
|
595
|
+
|
|
596
|
+
# Use CLI-provided templates if specified, else use workflow selection
|
|
597
|
+
final_templates = templates if templates else template_source
|
|
598
|
+
|
|
599
|
+
# Execute init with collected parameters
|
|
600
|
+
result = run_init(
|
|
601
|
+
path=path,
|
|
602
|
+
agents=final_agents,
|
|
603
|
+
update=update,
|
|
604
|
+
force=force,
|
|
605
|
+
yes=False,
|
|
606
|
+
template_source=final_templates,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Display results
|
|
610
|
+
display_init_result(result, final_agents or result.project.agents or [Agent.CLAUDE])
|
|
611
|
+
|
|
612
|
+
if not result.success:
|
|
613
|
+
raise typer.Exit(1)
|