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