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,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,10 @@
|
|
|
1
|
+
"""Roadmapit command wrapper for GitHub-integrated roadmap management.
|
|
2
|
+
|
|
3
|
+
This module imports and exposes the roadmapit command from doit_toolkit_cli
|
|
4
|
+
for use in the main doit CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from doit_toolkit_cli.commands.roadmapit import app as roadmapit_app
|
|
8
|
+
|
|
9
|
+
# Export the app for registration in main.py
|
|
10
|
+
app = roadmapit_app
|
|
@@ -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)
|