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