doit-toolkit-cli 0.1.9__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.
Files changed (134) 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/status_command.py +117 -0
  11. doit_cli/cli/sync_prompts_command.py +248 -0
  12. doit_cli/cli/validate_command.py +196 -0
  13. doit_cli/cli/verify_command.py +204 -0
  14. doit_cli/cli/workflow_mixin.py +224 -0
  15. doit_cli/cli/xref_command.py +555 -0
  16. doit_cli/formatters/__init__.py +8 -0
  17. doit_cli/formatters/base.py +38 -0
  18. doit_cli/formatters/json_formatter.py +126 -0
  19. doit_cli/formatters/markdown_formatter.py +97 -0
  20. doit_cli/formatters/rich_formatter.py +257 -0
  21. doit_cli/main.py +49 -0
  22. doit_cli/models/__init__.py +139 -0
  23. doit_cli/models/agent.py +74 -0
  24. doit_cli/models/analytics_models.py +384 -0
  25. doit_cli/models/context_config.py +464 -0
  26. doit_cli/models/crossref_models.py +182 -0
  27. doit_cli/models/diagram_models.py +363 -0
  28. doit_cli/models/fixit_models.py +355 -0
  29. doit_cli/models/hook_config.py +125 -0
  30. doit_cli/models/project.py +91 -0
  31. doit_cli/models/results.py +121 -0
  32. doit_cli/models/search_models.py +228 -0
  33. doit_cli/models/status_models.py +195 -0
  34. doit_cli/models/sync_models.py +146 -0
  35. doit_cli/models/template.py +77 -0
  36. doit_cli/models/validation_models.py +175 -0
  37. doit_cli/models/workflow_models.py +319 -0
  38. doit_cli/prompts/__init__.py +5 -0
  39. doit_cli/prompts/fixit_prompts.py +344 -0
  40. doit_cli/prompts/interactive.py +390 -0
  41. doit_cli/rules/__init__.py +5 -0
  42. doit_cli/rules/builtin_rules.py +160 -0
  43. doit_cli/services/__init__.py +79 -0
  44. doit_cli/services/agent_detector.py +168 -0
  45. doit_cli/services/analytics_service.py +218 -0
  46. doit_cli/services/architecture_generator.py +290 -0
  47. doit_cli/services/backup_service.py +204 -0
  48. doit_cli/services/config_loader.py +113 -0
  49. doit_cli/services/context_loader.py +1121 -0
  50. doit_cli/services/coverage_calculator.py +142 -0
  51. doit_cli/services/crossref_service.py +237 -0
  52. doit_cli/services/cycle_time_calculator.py +134 -0
  53. doit_cli/services/date_inferrer.py +349 -0
  54. doit_cli/services/diagram_service.py +337 -0
  55. doit_cli/services/drift_detector.py +109 -0
  56. doit_cli/services/entity_parser.py +301 -0
  57. doit_cli/services/er_diagram_generator.py +197 -0
  58. doit_cli/services/fixit_service.py +699 -0
  59. doit_cli/services/github_service.py +192 -0
  60. doit_cli/services/hook_manager.py +258 -0
  61. doit_cli/services/hook_validator.py +528 -0
  62. doit_cli/services/input_validator.py +322 -0
  63. doit_cli/services/memory_search.py +527 -0
  64. doit_cli/services/mermaid_validator.py +334 -0
  65. doit_cli/services/prompt_transformer.py +91 -0
  66. doit_cli/services/prompt_writer.py +133 -0
  67. doit_cli/services/query_interpreter.py +428 -0
  68. doit_cli/services/report_exporter.py +219 -0
  69. doit_cli/services/report_generator.py +256 -0
  70. doit_cli/services/requirement_parser.py +112 -0
  71. doit_cli/services/roadmap_summarizer.py +209 -0
  72. doit_cli/services/rule_engine.py +443 -0
  73. doit_cli/services/scaffolder.py +215 -0
  74. doit_cli/services/score_calculator.py +172 -0
  75. doit_cli/services/section_parser.py +204 -0
  76. doit_cli/services/spec_scanner.py +327 -0
  77. doit_cli/services/state_manager.py +355 -0
  78. doit_cli/services/status_reporter.py +143 -0
  79. doit_cli/services/task_parser.py +347 -0
  80. doit_cli/services/template_manager.py +710 -0
  81. doit_cli/services/template_reader.py +158 -0
  82. doit_cli/services/user_journey_generator.py +214 -0
  83. doit_cli/services/user_story_parser.py +232 -0
  84. doit_cli/services/validation_service.py +188 -0
  85. doit_cli/services/validator.py +232 -0
  86. doit_cli/services/velocity_tracker.py +173 -0
  87. doit_cli/services/workflow_engine.py +405 -0
  88. doit_cli/templates/agent-file-template.md +28 -0
  89. doit_cli/templates/checklist-template.md +39 -0
  90. doit_cli/templates/commands/doit.checkin.md +363 -0
  91. doit_cli/templates/commands/doit.constitution.md +187 -0
  92. doit_cli/templates/commands/doit.documentit.md +485 -0
  93. doit_cli/templates/commands/doit.fixit.md +181 -0
  94. doit_cli/templates/commands/doit.implementit.md +265 -0
  95. doit_cli/templates/commands/doit.planit.md +262 -0
  96. doit_cli/templates/commands/doit.reviewit.md +355 -0
  97. doit_cli/templates/commands/doit.roadmapit.md +368 -0
  98. doit_cli/templates/commands/doit.scaffoldit.md +458 -0
  99. doit_cli/templates/commands/doit.specit.md +521 -0
  100. doit_cli/templates/commands/doit.taskit.md +304 -0
  101. doit_cli/templates/commands/doit.testit.md +277 -0
  102. doit_cli/templates/config/context.yaml +134 -0
  103. doit_cli/templates/config/hooks.yaml +93 -0
  104. doit_cli/templates/config/validation-rules.yaml +64 -0
  105. doit_cli/templates/github-issue-templates/epic.yml +78 -0
  106. doit_cli/templates/github-issue-templates/feature.yml +116 -0
  107. doit_cli/templates/github-issue-templates/task.yml +129 -0
  108. doit_cli/templates/hooks/.gitkeep +0 -0
  109. doit_cli/templates/hooks/post-commit.sh +25 -0
  110. doit_cli/templates/hooks/post-merge.sh +75 -0
  111. doit_cli/templates/hooks/pre-commit.sh +17 -0
  112. doit_cli/templates/hooks/pre-push.sh +18 -0
  113. doit_cli/templates/memory/completed_roadmap.md +50 -0
  114. doit_cli/templates/memory/constitution.md +125 -0
  115. doit_cli/templates/memory/roadmap.md +61 -0
  116. doit_cli/templates/plan-template.md +146 -0
  117. doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
  118. doit_cli/templates/scripts/bash/common.sh +156 -0
  119. doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
  120. doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
  121. doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
  122. doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
  123. doit_cli/templates/scripts/powershell/common.ps1 +137 -0
  124. doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
  125. doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
  126. doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
  127. doit_cli/templates/spec-template.md +159 -0
  128. doit_cli/templates/tasks-template.md +313 -0
  129. doit_cli/templates/vscode-settings.json +14 -0
  130. doit_toolkit_cli-0.1.9.dist-info/METADATA +324 -0
  131. doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
  132. doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
  133. doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
  134. doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,293 @@
1
+ """CLI commands for memory search and query functionality.
2
+
3
+ This module provides the `doit memory` subcommand group for searching
4
+ across project context files.
5
+ """
6
+
7
+ import json
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import typer
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ from ..models.search_models import QueryType, SourceFilter
17
+ from ..services.memory_search import MemorySearchService
18
+
19
+ # Create the memory app
20
+ memory_app = typer.Typer(
21
+ name="memory",
22
+ help="Search and query project memory (constitution, roadmap, specs)",
23
+ add_completion=False,
24
+ )
25
+
26
+ console = Console()
27
+
28
+
29
+ def get_project_root() -> Path:
30
+ """Get the project root directory.
31
+
32
+ Walks up from current directory looking for .doit directory.
33
+
34
+ Returns:
35
+ Path to project root.
36
+
37
+ Raises:
38
+ typer.Exit: If no .doit directory found.
39
+ """
40
+ current = Path.cwd()
41
+ for parent in [current] + list(current.parents):
42
+ if (parent / ".doit").is_dir():
43
+ return parent
44
+
45
+ console.print(
46
+ "[red]Error:[/red] Not in a doit project. "
47
+ "Run 'doit init' to initialize."
48
+ )
49
+ raise typer.Exit(1)
50
+
51
+
52
+ @memory_app.command(name="search")
53
+ def search_command(
54
+ query: str = typer.Argument(
55
+ ...,
56
+ help="Search term, phrase, or natural language question",
57
+ ),
58
+ query_type: str = typer.Option(
59
+ "keyword",
60
+ "--type",
61
+ "-t",
62
+ help="Query type: keyword, phrase, natural, regex",
63
+ ),
64
+ source: str = typer.Option(
65
+ "all",
66
+ "--source",
67
+ "-s",
68
+ help="Source filter: all, governance, specs",
69
+ ),
70
+ max_results: int = typer.Option(
71
+ 20,
72
+ "--max",
73
+ "-m",
74
+ help="Maximum results to return (1-100)",
75
+ ),
76
+ case_sensitive: bool = typer.Option(
77
+ False,
78
+ "--case-sensitive",
79
+ "-c",
80
+ help="Enable case-sensitive matching",
81
+ ),
82
+ use_regex: bool = typer.Option(
83
+ False,
84
+ "--regex",
85
+ "-r",
86
+ help="Interpret query as regular expression",
87
+ ),
88
+ json_output: bool = typer.Option(
89
+ False,
90
+ "--json",
91
+ "-j",
92
+ help="Output results as JSON",
93
+ ),
94
+ ):
95
+ """Search across project memory files.
96
+
97
+ Search the constitution, roadmap, and spec files for keywords,
98
+ phrases, or ask natural language questions.
99
+
100
+ Examples:
101
+ doit memory search authentication
102
+ doit memory search -t phrase "user story"
103
+ doit memory search -t natural "what is the project vision?"
104
+ doit memory search -s specs "FR-001"
105
+ """
106
+ project_root = get_project_root()
107
+
108
+ # Validate and convert query type
109
+ try:
110
+ qt = QueryType(query_type.lower())
111
+ except ValueError:
112
+ console.print(
113
+ f"[red]Error:[/red] Invalid query type '{query_type}'. "
114
+ "Use: keyword, phrase, natural, regex"
115
+ )
116
+ raise typer.Exit(1)
117
+
118
+ # If regex flag is set, override query type
119
+ if use_regex:
120
+ qt = QueryType.REGEX
121
+
122
+ # Validate and convert source filter
123
+ try:
124
+ sf = SourceFilter(source.lower())
125
+ except ValueError:
126
+ console.print(
127
+ f"[red]Error:[/red] Invalid source filter '{source}'. "
128
+ "Use: all, governance, specs"
129
+ )
130
+ raise typer.Exit(1)
131
+
132
+ # Validate max results
133
+ if not 1 <= max_results <= 100:
134
+ console.print(
135
+ "[red]Error:[/red] Max results must be between 1 and 100"
136
+ )
137
+ raise typer.Exit(1)
138
+
139
+ # Create service and search
140
+ service = MemorySearchService(project_root, console)
141
+
142
+ start_time = time.time()
143
+ try:
144
+ results, sources, search_query = service.search(
145
+ query_text=query,
146
+ query_type=qt,
147
+ source_filter=sf,
148
+ max_results=max_results,
149
+ case_sensitive=case_sensitive,
150
+ use_regex=use_regex,
151
+ )
152
+ except ValueError as e:
153
+ console.print(f"[red]Error:[/red] {e}")
154
+ raise typer.Exit(3)
155
+
156
+ execution_time_ms = int((time.time() - start_time) * 1000)
157
+
158
+ # Output results
159
+ if json_output:
160
+ output = service.format_results_json(
161
+ results, sources, search_query, execution_time_ms
162
+ )
163
+ console.print_json(json.dumps(output, indent=2))
164
+ return
165
+
166
+ # Rich output
167
+ if not results:
168
+ console.print("\n[yellow]No results found.[/yellow]")
169
+ console.print(
170
+ f"\nSearched {len(sources)} files in {execution_time_ms}ms"
171
+ )
172
+ return
173
+
174
+ # Header
175
+ console.print()
176
+ console.print("[bold]Memory Search Results[/bold]")
177
+ console.print()
178
+
179
+ # Show interpretation info for natural language queries
180
+ if qt == QueryType.NATURAL and hasattr(search_query, "_interpreted"):
181
+ interpreted = search_query._interpreted
182
+ console.print(f'Query: "{query}" ({qt.value})')
183
+ console.print(f" ↳ Type: {interpreted.question_type.value}")
184
+ console.print(f" ↳ Keywords: {', '.join(interpreted.keywords[:5])}")
185
+ if interpreted.section_hints:
186
+ console.print(f" ↳ Sections: {', '.join(interpreted.section_hints[:3])}")
187
+ console.print(f" ↳ Confidence: {interpreted.confidence:.0%}")
188
+ else:
189
+ console.print(f'Query: "{query}" ({qt.value})')
190
+
191
+ console.print(
192
+ f"Sources: {sf.value} | Found: {len(results)} results"
193
+ )
194
+ console.print()
195
+
196
+ # Display results
197
+ source_map = {s.id: s for s in sources}
198
+ for result in results:
199
+ panel = service.format_result_rich(result, source_map, query)
200
+ console.print(panel)
201
+ console.print()
202
+
203
+ # Footer
204
+ console.print(
205
+ f"Searched {len(sources)} files in {execution_time_ms}ms"
206
+ )
207
+
208
+
209
+ @memory_app.command(name="history")
210
+ def history_command(
211
+ clear: bool = typer.Option(
212
+ False,
213
+ "--clear",
214
+ "-c",
215
+ help="Clear search history",
216
+ ),
217
+ json_output: bool = typer.Option(
218
+ False,
219
+ "--json",
220
+ "-j",
221
+ help="Output history as JSON",
222
+ ),
223
+ ):
224
+ """View or clear search history.
225
+
226
+ Shows recent searches from the current session.
227
+
228
+ Examples:
229
+ doit memory history
230
+ doit memory history --clear
231
+ doit memory history --json
232
+ """
233
+ project_root = get_project_root()
234
+ service = MemorySearchService(project_root, console)
235
+
236
+ if clear:
237
+ service.clear_history()
238
+ console.print("[green]Search history cleared.[/green]")
239
+ return
240
+
241
+ history = service.get_history()
242
+ entries = history.get_recent(10)
243
+
244
+ if json_output:
245
+ output = {
246
+ "session_id": history.session_id,
247
+ "session_start": history.session_start.isoformat(),
248
+ "entries": [
249
+ {
250
+ "query_text": q.query_text,
251
+ "query_type": q.query_type.value,
252
+ "timestamp": q.timestamp.isoformat(),
253
+ }
254
+ for q in entries
255
+ ],
256
+ "total_entries": len(history.entries),
257
+ }
258
+ console.print_json(json.dumps(output, indent=2))
259
+ return
260
+
261
+ if not entries:
262
+ console.print("\n[yellow]No search history.[/yellow]")
263
+ console.print(
264
+ "\nRun 'doit memory search <query>' to start searching."
265
+ )
266
+ return
267
+
268
+ console.print()
269
+ console.print("[bold]Search History (Session)[/bold]")
270
+ console.print()
271
+ console.print(f"Started: {history.session_start.strftime('%Y-%m-%d %H:%M:%S')}")
272
+ console.print()
273
+
274
+ # Create table
275
+ table = Table(show_header=True, header_style="bold")
276
+ table.add_column("#", justify="right", width=3)
277
+ table.add_column("Time", width=10)
278
+ table.add_column("Query", width=30)
279
+ table.add_column("Type", width=10)
280
+
281
+ for i, entry in enumerate(entries, 1):
282
+ time_str = entry.timestamp.strftime("%H:%M:%S")
283
+ query_display = entry.query_text[:27] + "..." if len(entry.query_text) > 30 else entry.query_text
284
+ table.add_row(
285
+ str(i),
286
+ time_str,
287
+ query_display,
288
+ entry.query_type.value,
289
+ )
290
+
291
+ console.print(table)
292
+ console.print()
293
+ console.print(f"{len(history.entries)} queries this session")
@@ -0,0 +1,117 @@
1
+ """Status command for displaying spec status dashboard."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+ from ..formatters.json_formatter import JsonFormatter
10
+ from ..formatters.markdown_formatter import MarkdownFormatter
11
+ from ..formatters.rich_formatter import RichFormatter
12
+ from ..models.status_models import SpecState
13
+ from ..services.spec_scanner import NotADoitProjectError
14
+ from ..services.status_reporter import StatusReporter
15
+
16
+ console = Console()
17
+
18
+ # Valid status filter values
19
+ VALID_STATUSES = ["draft", "in-progress", "complete", "approved"]
20
+
21
+
22
+ def status_command(
23
+ status_filter: Optional[str] = typer.Option(
24
+ None,
25
+ "--status",
26
+ "-s",
27
+ help="Filter by status (draft, in-progress, complete, approved)",
28
+ ),
29
+ blocking: bool = typer.Option(
30
+ False, "--blocking", "-b", help="Show only blocking specs"
31
+ ),
32
+ verbose: bool = typer.Option(
33
+ False, "--verbose", "-v", help="Show detailed validation errors"
34
+ ),
35
+ recent: Optional[int] = typer.Option(
36
+ None, "--recent", "-r", help="Show specs modified in last N days"
37
+ ),
38
+ output_format: str = typer.Option(
39
+ "rich", "--format", "-f", help="Output format: rich, json, markdown"
40
+ ),
41
+ output_file: Optional[Path] = typer.Option(
42
+ None, "--output", "-o", help="Write report to file"
43
+ ),
44
+ ) -> None:
45
+ """Display status of all specifications in the project.
46
+
47
+ Shows a dashboard of all specs with their status, validation results,
48
+ and commit blocking indicators.
49
+
50
+ Exit codes:
51
+ 0 - Success, no blocking specs
52
+ 1 - Success, but blocking specs exist
53
+ 2 - Error (not a doit project, invalid options)
54
+ """
55
+ try:
56
+ # Validate status filter
57
+ spec_state_filter = None
58
+ if status_filter:
59
+ status_lower = status_filter.lower()
60
+ if status_lower not in VALID_STATUSES:
61
+ console.print(
62
+ f"[red]Error:[/red] Invalid status '{status_filter}'. "
63
+ f"Valid: {', '.join(VALID_STATUSES)}"
64
+ )
65
+ raise typer.Exit(code=2)
66
+ spec_state_filter = SpecState.from_string(status_lower)
67
+
68
+ # Validate format
69
+ valid_formats = ["rich", "json", "markdown"]
70
+ if output_format not in valid_formats:
71
+ console.print(
72
+ f"[red]Error:[/red] Invalid format '{output_format}'. "
73
+ f"Valid: {', '.join(valid_formats)}"
74
+ )
75
+ raise typer.Exit(code=2)
76
+
77
+ # Initialize reporter
78
+ reporter = StatusReporter()
79
+
80
+ # Generate report with filters
81
+ report = reporter.generate_report(
82
+ status_filter=spec_state_filter,
83
+ blocking_only=blocking,
84
+ recent_days=recent,
85
+ )
86
+
87
+ # Select formatter based on format option
88
+ if output_format == "json":
89
+ formatter = JsonFormatter()
90
+ elif output_format == "markdown":
91
+ formatter = MarkdownFormatter()
92
+ else:
93
+ formatter = RichFormatter(console)
94
+
95
+ # Generate formatted output
96
+ output_str = formatter.format(report, verbose=verbose)
97
+
98
+ # Handle output destination
99
+ if output_file:
100
+ output_file.write_text(output_str)
101
+ console.print(f"[green]Report written to {output_file}[/green]")
102
+ elif output_format == "rich":
103
+ # Rich formatter should print directly for better terminal rendering
104
+ RichFormatter(console).format_to_console(report, verbose=verbose)
105
+ else:
106
+ # JSON and Markdown output to stdout - use print() for raw output
107
+ print(output_str)
108
+
109
+ # Determine exit code based on blocking status
110
+ if report.blocking_count > 0:
111
+ raise typer.Exit(code=1)
112
+ else:
113
+ raise typer.Exit(code=0)
114
+
115
+ except NotADoitProjectError as e:
116
+ console.print(f"[red]Error:[/red] {e}")
117
+ raise typer.Exit(code=2)
@@ -0,0 +1,248 @@
1
+ """Sync-prompts command for synchronizing GitHub Copilot prompts with doit commands."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated, Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ..models.sync_models import OperationType, SyncResult
12
+ from ..services.prompt_writer import PromptWriter
13
+ from ..services.template_reader import TemplateReader
14
+
15
+
16
+ console = Console()
17
+
18
+ # Type aliases for CLI options
19
+ JsonFlag = Annotated[
20
+ bool,
21
+ typer.Option(
22
+ "--json", "-j",
23
+ help="Output results as JSON"
24
+ )
25
+ ]
26
+
27
+ CheckFlag = Annotated[
28
+ bool,
29
+ typer.Option(
30
+ "--check", "-c",
31
+ help="Check sync status without making changes"
32
+ )
33
+ ]
34
+
35
+ ForceFlag = Annotated[
36
+ bool,
37
+ typer.Option(
38
+ "--force", "-f",
39
+ help="Force sync even if files are up-to-date"
40
+ )
41
+ ]
42
+
43
+
44
+ def get_operation_style(operation_type: OperationType) -> str:
45
+ """Get rich style for operation type."""
46
+ styles = {
47
+ OperationType.CREATED: "green",
48
+ OperationType.UPDATED: "yellow",
49
+ OperationType.SKIPPED: "dim",
50
+ OperationType.FAILED: "red",
51
+ }
52
+ return styles.get(operation_type, "white")
53
+
54
+
55
+ def get_operation_symbol(operation_type: OperationType) -> str:
56
+ """Get symbol for operation type."""
57
+ symbols = {
58
+ OperationType.CREATED: "+",
59
+ OperationType.UPDATED: "~",
60
+ OperationType.SKIPPED: "-",
61
+ OperationType.FAILED: "✗",
62
+ }
63
+ return symbols.get(operation_type, "?")
64
+
65
+
66
+ def display_sync_result(result: SyncResult) -> None:
67
+ """Display sync result with rich formatting.
68
+
69
+ Args:
70
+ result: The sync result to display.
71
+ """
72
+ if not result.operations:
73
+ console.print("[yellow]No command templates found to sync.[/yellow]")
74
+ return
75
+
76
+ # Create table
77
+ table = Table(
78
+ show_header=True,
79
+ header_style="bold cyan",
80
+ title="Prompt Synchronization Results",
81
+ )
82
+ table.add_column("Status", width=10, justify="center")
83
+ table.add_column("File", width=40)
84
+ table.add_column("Message")
85
+
86
+ for op in result.operations:
87
+ style = get_operation_style(op.operation_type)
88
+ symbol = get_operation_symbol(op.operation_type)
89
+ status_text = f"[{style}]{symbol} {op.operation_type.value.upper()}[/{style}]"
90
+
91
+ # Show just the filename, not full path
92
+ filename = Path(op.file_path).name
93
+
94
+ table.add_row(
95
+ status_text,
96
+ filename,
97
+ op.message,
98
+ )
99
+
100
+ console.print()
101
+ console.print(table)
102
+
103
+ # Summary
104
+ console.print()
105
+ console.print(
106
+ f"[bold]Summary:[/bold] "
107
+ f"{result.total_commands} commands, "
108
+ f"[green]{result.synced} synced[/green], "
109
+ f"[dim]{result.skipped} skipped[/dim], "
110
+ f"[red]{result.failed} failed[/red]"
111
+ )
112
+
113
+ # Final status
114
+ console.print()
115
+ if result.success:
116
+ if result.synced > 0:
117
+ console.print("[bold green]Prompts synchronized successfully[/bold green]")
118
+ else:
119
+ console.print("[green]All prompts are up-to-date[/green]")
120
+ else:
121
+ console.print("[bold red]Some prompts failed to sync[/bold red]")
122
+
123
+
124
+ def display_json_result(result: SyncResult) -> None:
125
+ """Display sync result as JSON.
126
+
127
+ Args:
128
+ result: The sync result to display.
129
+ """
130
+ output = {
131
+ "total_commands": result.total_commands,
132
+ "synced": result.synced,
133
+ "skipped": result.skipped,
134
+ "failed": result.failed,
135
+ "success": result.success,
136
+ "operations": [
137
+ {
138
+ "file_path": op.file_path,
139
+ "operation_type": op.operation_type.value,
140
+ "success": op.success,
141
+ "message": op.message,
142
+ }
143
+ for op in result.operations
144
+ ],
145
+ }
146
+ # Use print() instead of console.print() to avoid Rich's line wrapping
147
+ print(json.dumps(output, indent=2))
148
+
149
+
150
+ def sync_prompts_command(
151
+ command_name: Annotated[
152
+ Optional[str],
153
+ typer.Argument(
154
+ help="Specific command to sync (e.g., 'doit.checkin')"
155
+ )
156
+ ] = None,
157
+ check: CheckFlag = False,
158
+ force: ForceFlag = False,
159
+ json_output: JsonFlag = False,
160
+ path: Annotated[
161
+ Path,
162
+ typer.Option(
163
+ "--path", "-p",
164
+ help="Project directory path"
165
+ )
166
+ ] = Path("."),
167
+ ) -> None:
168
+ """Synchronize GitHub Copilot prompts with doit command templates.
169
+
170
+ Reads command templates from .doit/templates/commands/ and generates
171
+ corresponding prompt files in .github/prompts/ with naming convention
172
+ doit.<name>.prompt.md.
173
+
174
+ Examples:
175
+ doit sync-prompts # Sync all commands
176
+ doit sync-prompts doit.checkin # Sync specific command
177
+ doit sync-prompts --check # Check sync status only
178
+ doit sync-prompts --force # Force re-sync all
179
+ doit sync-prompts --json # Output as JSON
180
+ """
181
+ project_root = path.resolve()
182
+
183
+ # Initialize services
184
+ reader = TemplateReader(project_root=project_root)
185
+ writer = PromptWriter(project_root=project_root)
186
+
187
+ # Read templates
188
+ templates = reader.scan_templates(filter_name=command_name)
189
+
190
+ if not templates:
191
+ if command_name:
192
+ msg = f"Command template '{command_name}' not found"
193
+ else:
194
+ msg = "No command templates found in .doit/templates/commands/"
195
+
196
+ if json_output:
197
+ console.print(json.dumps({"error": msg}))
198
+ else:
199
+ console.print(f"[red]Error:[/red] {msg}")
200
+ raise typer.Exit(1)
201
+
202
+ # Check mode - just report status without making changes
203
+ if check:
204
+ result = SyncResult(total_commands=len(templates))
205
+ for template in templates:
206
+ prompt_path = writer.get_prompt_path(template)
207
+
208
+ if not prompt_path.exists():
209
+ from ..models.sync_models import FileOperation
210
+ result.add_operation(FileOperation(
211
+ file_path=str(prompt_path),
212
+ operation_type=OperationType.FAILED,
213
+ success=False,
214
+ message="Missing - needs sync",
215
+ ))
216
+ else:
217
+ prompt_mtime = prompt_path.stat().st_mtime
218
+ template_mtime = template.modified_at.timestamp()
219
+
220
+ if prompt_mtime < template_mtime:
221
+ from ..models.sync_models import FileOperation
222
+ result.add_operation(FileOperation(
223
+ file_path=str(prompt_path),
224
+ operation_type=OperationType.UPDATED,
225
+ success=True,
226
+ message="Out-of-sync - needs update",
227
+ ))
228
+ else:
229
+ from ..models.sync_models import FileOperation
230
+ result.add_operation(FileOperation(
231
+ file_path=str(prompt_path),
232
+ operation_type=OperationType.SKIPPED,
233
+ success=True,
234
+ message="Up-to-date",
235
+ ))
236
+ else:
237
+ # Perform actual sync
238
+ result = writer.write_prompts(templates, force=force)
239
+
240
+ # Display results
241
+ if json_output:
242
+ display_json_result(result)
243
+ else:
244
+ display_sync_result(result)
245
+
246
+ # Exit with appropriate code
247
+ if not result.success:
248
+ raise typer.Exit(1)