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.
- 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/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 +49 -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 +1121 -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 +368 -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.9.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
- doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Validate command for spec file linting and quality checking."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from ..models.validation_models import ValidationConfig
|
|
10
|
+
from ..services.report_generator import ReportGenerator
|
|
11
|
+
from ..services.validation_service import ValidationService
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
# Type aliases for CLI options
|
|
17
|
+
JsonFlag = Annotated[
|
|
18
|
+
bool,
|
|
19
|
+
typer.Option(
|
|
20
|
+
"--json", "-j",
|
|
21
|
+
help="Output results as JSON"
|
|
22
|
+
)
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
AllFlag = Annotated[
|
|
26
|
+
bool,
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--all", "-a",
|
|
29
|
+
help="Validate all specs in specs/ directory"
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
VerboseFlag = Annotated[
|
|
34
|
+
bool,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--verbose", "-v",
|
|
37
|
+
help="Show detailed output including all issues"
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def validate_command(
|
|
43
|
+
path: Annotated[
|
|
44
|
+
Optional[Path],
|
|
45
|
+
typer.Argument(
|
|
46
|
+
help="Spec file or directory to validate (defaults to current directory)"
|
|
47
|
+
)
|
|
48
|
+
] = None,
|
|
49
|
+
all_specs: AllFlag = False,
|
|
50
|
+
json_output: JsonFlag = False,
|
|
51
|
+
verbose: VerboseFlag = False,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Validate spec files for quality and standards compliance.
|
|
54
|
+
|
|
55
|
+
Checks spec files against validation rules including:
|
|
56
|
+
- Required sections (User Scenarios, Requirements, Success Criteria)
|
|
57
|
+
- Naming conventions (FR-XXX, SC-XXX patterns)
|
|
58
|
+
- Acceptance scenario format (Given/When/Then)
|
|
59
|
+
- Clarity markers ([NEEDS CLARIFICATION], TODO)
|
|
60
|
+
|
|
61
|
+
Custom rules can be configured via .doit/validation-rules.yaml:
|
|
62
|
+
- disabled_rules: List of rule IDs to skip
|
|
63
|
+
- overrides: Change severity of specific rules
|
|
64
|
+
- custom_rules: Add project-specific pattern checks
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
doit validate # Validate current spec
|
|
68
|
+
doit validate specs/001-feature/ # Validate specific spec directory
|
|
69
|
+
doit validate spec.md # Validate specific file
|
|
70
|
+
doit validate --all # Validate all specs
|
|
71
|
+
doit validate --all --json # Output as JSON
|
|
72
|
+
"""
|
|
73
|
+
# Resolve path
|
|
74
|
+
project_root = Path.cwd()
|
|
75
|
+
|
|
76
|
+
if path:
|
|
77
|
+
target_path = path if path.is_absolute() else project_root / path
|
|
78
|
+
else:
|
|
79
|
+
target_path = project_root
|
|
80
|
+
|
|
81
|
+
# Create services
|
|
82
|
+
config = ValidationConfig.default()
|
|
83
|
+
service = ValidationService(project_root=project_root, config=config)
|
|
84
|
+
reporter = ReportGenerator(console=console)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
if all_specs:
|
|
88
|
+
# Validate all specs in specs/ directory
|
|
89
|
+
results = service.validate_all()
|
|
90
|
+
|
|
91
|
+
if not results:
|
|
92
|
+
if json_output:
|
|
93
|
+
print('{"error": "No spec files found in specs/ directory"}')
|
|
94
|
+
else:
|
|
95
|
+
console.print("[yellow]No spec files found in specs/ directory[/yellow]")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
summary = service.get_summary(results)
|
|
99
|
+
|
|
100
|
+
if json_output:
|
|
101
|
+
print(reporter.to_json_summary(results, summary))
|
|
102
|
+
else:
|
|
103
|
+
if verbose:
|
|
104
|
+
# Show each result in detail
|
|
105
|
+
for result in results:
|
|
106
|
+
reporter.display_result(result)
|
|
107
|
+
console.print()
|
|
108
|
+
|
|
109
|
+
# Always show summary for --all
|
|
110
|
+
reporter.display_summary(results, summary)
|
|
111
|
+
|
|
112
|
+
# Exit with error if any specs failed
|
|
113
|
+
if summary["failed"] > 0:
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
elif target_path.is_file():
|
|
117
|
+
# Validate single file
|
|
118
|
+
if not target_path.suffix.lower() == ".md":
|
|
119
|
+
if json_output:
|
|
120
|
+
print('{"error": "Not a markdown file"}')
|
|
121
|
+
else:
|
|
122
|
+
console.print(f"[red]Error:[/red] Not a markdown file: {target_path}")
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
|
|
125
|
+
result = service.validate_file(target_path)
|
|
126
|
+
|
|
127
|
+
if json_output:
|
|
128
|
+
print(reporter.to_json(result))
|
|
129
|
+
else:
|
|
130
|
+
reporter.display_result(result)
|
|
131
|
+
|
|
132
|
+
# Exit with error if validation failed
|
|
133
|
+
if result.error_count > 0:
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
elif target_path.is_dir():
|
|
137
|
+
# Check if this is a spec directory (contains spec.md)
|
|
138
|
+
spec_file = target_path / "spec.md"
|
|
139
|
+
|
|
140
|
+
if spec_file.exists():
|
|
141
|
+
# Validate single spec directory
|
|
142
|
+
result = service.validate_file(spec_file)
|
|
143
|
+
|
|
144
|
+
if json_output:
|
|
145
|
+
print(reporter.to_json(result))
|
|
146
|
+
else:
|
|
147
|
+
reporter.display_result(result)
|
|
148
|
+
|
|
149
|
+
if result.error_count > 0:
|
|
150
|
+
raise typer.Exit(1)
|
|
151
|
+
else:
|
|
152
|
+
# Validate all specs in the directory
|
|
153
|
+
results = service.validate_directory(target_path)
|
|
154
|
+
|
|
155
|
+
if not results:
|
|
156
|
+
if json_output:
|
|
157
|
+
print('{"error": "No spec files found in directory"}')
|
|
158
|
+
else:
|
|
159
|
+
console.print(f"[yellow]No spec files found in {target_path}[/yellow]")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
summary = service.get_summary(results)
|
|
163
|
+
|
|
164
|
+
if json_output:
|
|
165
|
+
print(reporter.to_json_summary(results, summary))
|
|
166
|
+
else:
|
|
167
|
+
if verbose:
|
|
168
|
+
for result in results:
|
|
169
|
+
reporter.display_result(result)
|
|
170
|
+
console.print()
|
|
171
|
+
|
|
172
|
+
reporter.display_summary(results, summary)
|
|
173
|
+
|
|
174
|
+
if summary["failed"] > 0:
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
if json_output:
|
|
179
|
+
print(f'{{"error": "Path not found: {target_path}"}}')
|
|
180
|
+
else:
|
|
181
|
+
console.print(f"[red]Error:[/red] Path not found: {target_path}")
|
|
182
|
+
raise typer.Exit(1)
|
|
183
|
+
|
|
184
|
+
except FileNotFoundError as e:
|
|
185
|
+
if json_output:
|
|
186
|
+
print(f'{{"error": "{e}"}}')
|
|
187
|
+
else:
|
|
188
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
189
|
+
raise typer.Exit(1)
|
|
190
|
+
|
|
191
|
+
except ValueError as e:
|
|
192
|
+
if json_output:
|
|
193
|
+
print(f'{{"error": "{e}"}}')
|
|
194
|
+
else:
|
|
195
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
196
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Verify command for checking doit project setup."""
|
|
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.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ..models.agent import Agent
|
|
13
|
+
from ..models.project import Project
|
|
14
|
+
from ..models.results import VerifyResult, VerifyStatus
|
|
15
|
+
from ..services.validator import Validator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
# Type aliases for CLI options
|
|
21
|
+
JsonFlag = Annotated[
|
|
22
|
+
bool,
|
|
23
|
+
typer.Option(
|
|
24
|
+
"--json", "-j",
|
|
25
|
+
help="Output results as JSON"
|
|
26
|
+
)
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
AgentOption = Annotated[
|
|
30
|
+
Optional[str],
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--agent", "-a",
|
|
33
|
+
help="Specific agent to check: claude, copilot, or both"
|
|
34
|
+
)
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_agent_string(agent_str: str) -> list[Agent]:
|
|
39
|
+
"""Parse agent string into list of Agent enums.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent_str: Comma-separated agent names
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List of Agent enums
|
|
46
|
+
"""
|
|
47
|
+
agents = []
|
|
48
|
+
for name in agent_str.lower().split(","):
|
|
49
|
+
name = name.strip()
|
|
50
|
+
if name == "claude":
|
|
51
|
+
agents.append(Agent.CLAUDE)
|
|
52
|
+
elif name == "copilot":
|
|
53
|
+
agents.append(Agent.COPILOT)
|
|
54
|
+
else:
|
|
55
|
+
raise typer.BadParameter(
|
|
56
|
+
f"Unknown agent: {name}. Use 'claude', 'copilot', or 'claude,copilot'"
|
|
57
|
+
)
|
|
58
|
+
return agents
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_status_style(status: VerifyStatus) -> str:
|
|
62
|
+
"""Get rich style for verification status."""
|
|
63
|
+
if status == VerifyStatus.PASS:
|
|
64
|
+
return "green"
|
|
65
|
+
elif status == VerifyStatus.WARN:
|
|
66
|
+
return "yellow"
|
|
67
|
+
else: # FAIL
|
|
68
|
+
return "red"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_status_symbol(status: VerifyStatus) -> str:
|
|
72
|
+
"""Get symbol for verification status."""
|
|
73
|
+
if status == VerifyStatus.PASS:
|
|
74
|
+
return "✓"
|
|
75
|
+
elif status == VerifyStatus.WARN:
|
|
76
|
+
return "!"
|
|
77
|
+
else: # FAIL
|
|
78
|
+
return "✗"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def display_verify_result(result: VerifyResult) -> None:
|
|
82
|
+
"""Display verification result with rich formatting.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
result: The verification result to display
|
|
86
|
+
"""
|
|
87
|
+
# Create table
|
|
88
|
+
table = Table(
|
|
89
|
+
show_header=True,
|
|
90
|
+
header_style="bold cyan",
|
|
91
|
+
title="Project Verification Results",
|
|
92
|
+
)
|
|
93
|
+
table.add_column("Status", width=8, justify="center")
|
|
94
|
+
table.add_column("Check", width=20)
|
|
95
|
+
table.add_column("Message")
|
|
96
|
+
|
|
97
|
+
for check in result.checks:
|
|
98
|
+
style = get_status_style(check.status)
|
|
99
|
+
symbol = get_status_symbol(check.status)
|
|
100
|
+
status_text = f"[{style}]{symbol} {check.status.value.upper()}[/{style}]"
|
|
101
|
+
|
|
102
|
+
table.add_row(
|
|
103
|
+
status_text,
|
|
104
|
+
check.name,
|
|
105
|
+
check.message,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
console.print()
|
|
109
|
+
console.print(table)
|
|
110
|
+
|
|
111
|
+
# Summary
|
|
112
|
+
console.print()
|
|
113
|
+
console.print(f"[bold]Summary:[/bold] {result.summary}")
|
|
114
|
+
|
|
115
|
+
# Show suggestions if there are warnings or failures
|
|
116
|
+
suggestions = [
|
|
117
|
+
c.suggestion for c in result.checks
|
|
118
|
+
if c.suggestion and c.status != VerifyStatus.PASS
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
if suggestions:
|
|
122
|
+
console.print()
|
|
123
|
+
console.print(
|
|
124
|
+
Panel(
|
|
125
|
+
"\n".join(f"• {s}" for s in suggestions),
|
|
126
|
+
title="[yellow]Suggestions[/yellow]",
|
|
127
|
+
border_style="yellow",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Final status
|
|
132
|
+
console.print()
|
|
133
|
+
if result.passed:
|
|
134
|
+
if result.has_warnings:
|
|
135
|
+
console.print("[yellow]Project setup complete with warnings[/yellow]")
|
|
136
|
+
else:
|
|
137
|
+
console.print("[bold green]Project setup verified successfully[/bold green]")
|
|
138
|
+
else:
|
|
139
|
+
console.print("[bold red]Project setup has issues that need attention[/bold red]")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def display_json_result(result: VerifyResult) -> None:
|
|
143
|
+
"""Display verification result as JSON.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
result: The verification result to display
|
|
147
|
+
"""
|
|
148
|
+
output = result.to_dict()
|
|
149
|
+
console.print(json.dumps(output, indent=2, default=str))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def verify_command(
|
|
153
|
+
path: Annotated[
|
|
154
|
+
Path,
|
|
155
|
+
typer.Argument(
|
|
156
|
+
default=...,
|
|
157
|
+
help="Project directory path (use '.' for current directory)"
|
|
158
|
+
)
|
|
159
|
+
] = Path("."),
|
|
160
|
+
agent: AgentOption = None,
|
|
161
|
+
json_output: JsonFlag = False,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Verify doit project setup and report status.
|
|
164
|
+
|
|
165
|
+
Checks for:
|
|
166
|
+
- .doit/ folder structure
|
|
167
|
+
- Agent command directories
|
|
168
|
+
- Command template files
|
|
169
|
+
- Project constitution and roadmap
|
|
170
|
+
- Copilot-specific configuration (if applicable)
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
doit verify . # Verify current directory
|
|
174
|
+
doit verify . --agent claude # Check only Claude setup
|
|
175
|
+
doit verify . --json # Output as JSON
|
|
176
|
+
"""
|
|
177
|
+
# Parse agent string if provided
|
|
178
|
+
agents = None
|
|
179
|
+
if agent:
|
|
180
|
+
try:
|
|
181
|
+
agents = parse_agent_string(agent)
|
|
182
|
+
except typer.BadParameter as e:
|
|
183
|
+
if json_output:
|
|
184
|
+
console.print(json.dumps({"error": str(e)}))
|
|
185
|
+
else:
|
|
186
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
187
|
+
raise typer.Exit(1)
|
|
188
|
+
|
|
189
|
+
# Create project and validator
|
|
190
|
+
project = Project(path=path.resolve())
|
|
191
|
+
validator = Validator(project)
|
|
192
|
+
|
|
193
|
+
# Run verification
|
|
194
|
+
result = validator.run_all_checks(agents=agents)
|
|
195
|
+
|
|
196
|
+
# Display results
|
|
197
|
+
if json_output:
|
|
198
|
+
display_json_result(result)
|
|
199
|
+
else:
|
|
200
|
+
display_verify_result(result)
|
|
201
|
+
|
|
202
|
+
# Exit with appropriate code
|
|
203
|
+
if not result.passed:
|
|
204
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Workflow mixin for CLI commands.
|
|
2
|
+
|
|
3
|
+
This module provides a mixin class that adds guided workflow functionality
|
|
4
|
+
to Typer CLI commands, including --non-interactive flag support and
|
|
5
|
+
default value handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Callable, TypeVar
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from ..models.workflow_models import Workflow, WorkflowStep
|
|
15
|
+
from ..prompts.interactive import InteractivePrompt
|
|
16
|
+
from ..services.workflow_engine import WorkflowEngine
|
|
17
|
+
from ..services.state_manager import StateManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Type variable for decorator
|
|
21
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WorkflowMixin:
|
|
25
|
+
"""Mixin providing guided workflow functionality for CLI commands.
|
|
26
|
+
|
|
27
|
+
This class adds workflow execution capabilities to CLI commands,
|
|
28
|
+
including interactive prompting, progress display, and state recovery.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
class MyCommand(WorkflowMixin):
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.workflow = Workflow(...)
|
|
35
|
+
|
|
36
|
+
def run(self, non_interactive: bool = False):
|
|
37
|
+
self.init_workflow(non_interactive=non_interactive)
|
|
38
|
+
results = self.execute_workflow(self.workflow)
|
|
39
|
+
# Use results...
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
console: Console | None = None,
|
|
45
|
+
state_dir: str | None = None,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize the workflow mixin.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
console: Rich console for output
|
|
51
|
+
state_dir: Directory for state files (defaults to .doit/state)
|
|
52
|
+
"""
|
|
53
|
+
self.console = console or Console()
|
|
54
|
+
self._state_dir = state_dir
|
|
55
|
+
self._engine: WorkflowEngine | None = None
|
|
56
|
+
self._non_interactive = False
|
|
57
|
+
|
|
58
|
+
def init_workflow(
|
|
59
|
+
self,
|
|
60
|
+
non_interactive: bool = False,
|
|
61
|
+
no_state: bool = False,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Initialize the workflow engine.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
non_interactive: Force non-interactive mode
|
|
67
|
+
no_state: Disable state persistence
|
|
68
|
+
"""
|
|
69
|
+
self._non_interactive = non_interactive
|
|
70
|
+
|
|
71
|
+
# Check environment variable if not explicitly set
|
|
72
|
+
if not non_interactive:
|
|
73
|
+
env_value = os.environ.get("DOIT_NON_INTERACTIVE", "").lower()
|
|
74
|
+
self._non_interactive = env_value in ("true", "1", "yes")
|
|
75
|
+
|
|
76
|
+
# Create state manager if enabled
|
|
77
|
+
state_manager = None
|
|
78
|
+
if not no_state:
|
|
79
|
+
state_manager = StateManager(state_dir=self._state_dir)
|
|
80
|
+
|
|
81
|
+
# Create prompt with non-interactive flag
|
|
82
|
+
prompt = InteractivePrompt(
|
|
83
|
+
console=self.console,
|
|
84
|
+
force_non_interactive=self._non_interactive,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Create engine
|
|
88
|
+
self._engine = WorkflowEngine(
|
|
89
|
+
console=self.console,
|
|
90
|
+
state_manager=state_manager,
|
|
91
|
+
)
|
|
92
|
+
self._engine.prompt = prompt
|
|
93
|
+
|
|
94
|
+
def execute_workflow(self, workflow: Workflow) -> dict[str, str]:
|
|
95
|
+
"""Execute a guided workflow.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
workflow: Workflow definition to execute
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dictionary of step_id -> response value
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If engine not initialized
|
|
105
|
+
WorkflowError: If workflow fails
|
|
106
|
+
"""
|
|
107
|
+
if self._engine is None:
|
|
108
|
+
raise ValueError("Workflow engine not initialized. Call init_workflow() first.")
|
|
109
|
+
|
|
110
|
+
return self._engine.run(workflow)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_non_interactive(self) -> bool:
|
|
114
|
+
"""Check if running in non-interactive mode."""
|
|
115
|
+
return self._non_interactive
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def non_interactive_option() -> Callable[[F], F]:
|
|
119
|
+
"""Decorator that adds --non-interactive option to a command.
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
@app.command()
|
|
123
|
+
@non_interactive_option()
|
|
124
|
+
def my_command(non_interactive: bool = False):
|
|
125
|
+
...
|
|
126
|
+
"""
|
|
127
|
+
def decorator(func: F) -> F:
|
|
128
|
+
return typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--non-interactive",
|
|
131
|
+
"-n",
|
|
132
|
+
help="Run without interactive prompts, using default values.",
|
|
133
|
+
envvar="DOIT_NON_INTERACTIVE",
|
|
134
|
+
)(func)
|
|
135
|
+
return decorator
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def workflow_command_options(
|
|
139
|
+
func: Callable[..., Any] | None = None,
|
|
140
|
+
) -> Callable[[F], F]:
|
|
141
|
+
"""Add standard workflow options to a command.
|
|
142
|
+
|
|
143
|
+
Adds the following options:
|
|
144
|
+
- --non-interactive / -n: Disable interactive prompts
|
|
145
|
+
- --no-resume: Don't prompt to resume interrupted workflow
|
|
146
|
+
|
|
147
|
+
Usage:
|
|
148
|
+
@app.command()
|
|
149
|
+
@workflow_command_options
|
|
150
|
+
def my_command(
|
|
151
|
+
non_interactive: bool = typer.Option(False),
|
|
152
|
+
no_resume: bool = typer.Option(False),
|
|
153
|
+
):
|
|
154
|
+
...
|
|
155
|
+
"""
|
|
156
|
+
def decorator(f: F) -> F:
|
|
157
|
+
# This would add the options via typer decorators
|
|
158
|
+
# Implementation depends on how typer handles stacked decorators
|
|
159
|
+
return f
|
|
160
|
+
|
|
161
|
+
if func is not None:
|
|
162
|
+
return decorator(func)
|
|
163
|
+
return decorator
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_required_defaults(workflow: Workflow) -> list[str]:
|
|
167
|
+
"""Validate that all required steps have defaults for non-interactive mode.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
workflow: Workflow to validate
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of step IDs that are required but have no default
|
|
174
|
+
"""
|
|
175
|
+
missing = []
|
|
176
|
+
for step in workflow.steps:
|
|
177
|
+
if step.required and step.default_value is None:
|
|
178
|
+
missing.append(step.id)
|
|
179
|
+
return missing
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def create_non_interactive_workflow(
|
|
183
|
+
original: Workflow,
|
|
184
|
+
overrides: dict[str, str] | None = None,
|
|
185
|
+
) -> Workflow:
|
|
186
|
+
"""Create a workflow with defaults for non-interactive execution.
|
|
187
|
+
|
|
188
|
+
This creates a modified workflow where required steps that don't
|
|
189
|
+
have defaults get values from the overrides dict.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
original: Original workflow definition
|
|
193
|
+
overrides: Step ID to default value mapping
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Modified workflow suitable for non-interactive mode
|
|
197
|
+
"""
|
|
198
|
+
overrides = overrides or {}
|
|
199
|
+
new_steps = []
|
|
200
|
+
|
|
201
|
+
for step in original.steps:
|
|
202
|
+
if step.required and step.default_value is None and step.id in overrides:
|
|
203
|
+
# Create new step with override as default
|
|
204
|
+
new_step = WorkflowStep(
|
|
205
|
+
id=step.id,
|
|
206
|
+
name=step.name,
|
|
207
|
+
prompt_text=step.prompt_text,
|
|
208
|
+
required=step.required,
|
|
209
|
+
order=step.order,
|
|
210
|
+
validation_type=step.validation_type,
|
|
211
|
+
default_value=overrides[step.id],
|
|
212
|
+
options=step.options,
|
|
213
|
+
)
|
|
214
|
+
new_steps.append(new_step)
|
|
215
|
+
else:
|
|
216
|
+
new_steps.append(step)
|
|
217
|
+
|
|
218
|
+
return Workflow(
|
|
219
|
+
id=original.id,
|
|
220
|
+
command_name=original.command_name,
|
|
221
|
+
description=original.description,
|
|
222
|
+
interactive=original.interactive,
|
|
223
|
+
steps=new_steps,
|
|
224
|
+
)
|