specfact-cli 0.4.0__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 specfact-cli might be problematic. Click here for more details.
- specfact_cli/__init__.py +14 -0
- specfact_cli/agents/__init__.py +23 -0
- specfact_cli/agents/analyze_agent.py +392 -0
- specfact_cli/agents/base.py +95 -0
- specfact_cli/agents/plan_agent.py +202 -0
- specfact_cli/agents/registry.py +176 -0
- specfact_cli/agents/sync_agent.py +133 -0
- specfact_cli/analyzers/__init__.py +10 -0
- specfact_cli/analyzers/code_analyzer.py +775 -0
- specfact_cli/cli.py +397 -0
- specfact_cli/commands/__init__.py +7 -0
- specfact_cli/commands/enforce.py +87 -0
- specfact_cli/commands/import_cmd.py +355 -0
- specfact_cli/commands/init.py +119 -0
- specfact_cli/commands/plan.py +1090 -0
- specfact_cli/commands/repro.py +172 -0
- specfact_cli/commands/sync.py +408 -0
- specfact_cli/common/__init__.py +24 -0
- specfact_cli/common/logger_setup.py +673 -0
- specfact_cli/common/logging_utils.py +41 -0
- specfact_cli/common/text_utils.py +52 -0
- specfact_cli/common/utils.py +48 -0
- specfact_cli/comparators/__init__.py +10 -0
- specfact_cli/comparators/plan_comparator.py +391 -0
- specfact_cli/generators/__init__.py +13 -0
- specfact_cli/generators/plan_generator.py +105 -0
- specfact_cli/generators/protocol_generator.py +115 -0
- specfact_cli/generators/report_generator.py +200 -0
- specfact_cli/generators/workflow_generator.py +111 -0
- specfact_cli/importers/__init__.py +6 -0
- specfact_cli/importers/speckit_converter.py +773 -0
- specfact_cli/importers/speckit_scanner.py +704 -0
- specfact_cli/models/__init__.py +32 -0
- specfact_cli/models/deviation.py +105 -0
- specfact_cli/models/enforcement.py +150 -0
- specfact_cli/models/plan.py +97 -0
- specfact_cli/models/protocol.py +28 -0
- specfact_cli/modes/__init__.py +18 -0
- specfact_cli/modes/detector.py +126 -0
- specfact_cli/modes/router.py +153 -0
- specfact_cli/sync/__init__.py +11 -0
- specfact_cli/sync/repository_sync.py +279 -0
- specfact_cli/sync/speckit_sync.py +388 -0
- specfact_cli/utils/__init__.py +57 -0
- specfact_cli/utils/console.py +69 -0
- specfact_cli/utils/feature_keys.py +213 -0
- specfact_cli/utils/git.py +241 -0
- specfact_cli/utils/ide_setup.py +381 -0
- specfact_cli/utils/prompts.py +179 -0
- specfact_cli/utils/structure.py +496 -0
- specfact_cli/utils/yaml_utils.py +200 -0
- specfact_cli/validators/__init__.py +19 -0
- specfact_cli/validators/fsm.py +260 -0
- specfact_cli/validators/repro_checker.py +320 -0
- specfact_cli/validators/schema.py +200 -0
- specfact_cli-0.4.0.dist-info/METADATA +332 -0
- specfact_cli-0.4.0.dist-info/RECORD +60 -0
- specfact_cli-0.4.0.dist-info/WHEEL +4 -0
- specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
- specfact_cli-0.4.0.dist-info/licenses/LICENSE.md +55 -0
specfact_cli/cli.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SpecFact CLI - Main application entry point.
|
|
3
|
+
|
|
4
|
+
This module defines the main Typer application and registers all command groups.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from beartype import beartype
|
|
16
|
+
from icontract import ViolationError
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
|
|
20
|
+
from specfact_cli import __version__
|
|
21
|
+
|
|
22
|
+
# Import command modules
|
|
23
|
+
from specfact_cli.commands import enforce, import_cmd, init, plan, repro, sync
|
|
24
|
+
from specfact_cli.modes import OperationalMode, detect_mode
|
|
25
|
+
|
|
26
|
+
# Map shell names for completion support
|
|
27
|
+
SHELL_MAP = {
|
|
28
|
+
"sh": "bash", # sh is bash-compatible
|
|
29
|
+
"bash": "bash",
|
|
30
|
+
"zsh": "zsh",
|
|
31
|
+
"fish": "fish",
|
|
32
|
+
"powershell": "powershell",
|
|
33
|
+
"pwsh": "powershell", # PowerShell Core
|
|
34
|
+
"ps1": "powershell", # PowerShell alias
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_shell_in_argv() -> None:
|
|
39
|
+
"""Normalize shell names in sys.argv before Typer processes them."""
|
|
40
|
+
if len(sys.argv) >= 3:
|
|
41
|
+
# Check for --show-completion or --install-completion
|
|
42
|
+
if sys.argv[1] in ("--show-completion", "--install-completion"):
|
|
43
|
+
shell_arg = sys.argv[2]
|
|
44
|
+
shell_normalized = shell_arg.lower().strip()
|
|
45
|
+
mapped_shell = SHELL_MAP.get(shell_normalized)
|
|
46
|
+
if mapped_shell and mapped_shell != shell_normalized:
|
|
47
|
+
# Replace "sh" with "bash" in argv
|
|
48
|
+
sys.argv[2] = mapped_shell
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Note: Shell normalization happens in cli_main() before app() is called
|
|
52
|
+
# We don't normalize at module load time because sys.argv may not be set yet
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
app = typer.Typer(
|
|
56
|
+
name="specfact",
|
|
57
|
+
help="SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development",
|
|
58
|
+
add_completion=False, # Disable built-in completion (we provide custom commands with shell normalization)
|
|
59
|
+
rich_markup_mode="rich",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
console = Console()
|
|
63
|
+
|
|
64
|
+
# Global mode context (set by --mode flag or auto-detected)
|
|
65
|
+
_current_mode: OperationalMode | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def version_callback(value: bool) -> None:
|
|
69
|
+
"""Show version information."""
|
|
70
|
+
if value:
|
|
71
|
+
console.print(f"[bold cyan]SpecFact CLI[/bold cyan] version [green]{__version__}[/green]")
|
|
72
|
+
raise typer.Exit()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def mode_callback(value: Optional[str]) -> None:
|
|
76
|
+
"""Handle --mode flag callback."""
|
|
77
|
+
global _current_mode
|
|
78
|
+
if value is not None:
|
|
79
|
+
try:
|
|
80
|
+
_current_mode = OperationalMode(value.lower())
|
|
81
|
+
except ValueError:
|
|
82
|
+
console.print(f"[bold red]✗[/bold red] Invalid mode: {value}")
|
|
83
|
+
console.print("Valid modes: cicd, copilot")
|
|
84
|
+
raise typer.Exit(1) from None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@beartype
|
|
88
|
+
def get_current_mode() -> OperationalMode:
|
|
89
|
+
"""
|
|
90
|
+
Get the current operational mode.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Current operational mode (detected or explicit)
|
|
94
|
+
"""
|
|
95
|
+
global _current_mode
|
|
96
|
+
if _current_mode is not None:
|
|
97
|
+
return _current_mode
|
|
98
|
+
# Auto-detect if not explicitly set
|
|
99
|
+
_current_mode = detect_mode(explicit_mode=None)
|
|
100
|
+
return _current_mode
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.callback(invoke_without_command=True)
|
|
104
|
+
def main(
|
|
105
|
+
ctx: typer.Context,
|
|
106
|
+
version: bool = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
"--version",
|
|
109
|
+
"-v",
|
|
110
|
+
callback=version_callback,
|
|
111
|
+
is_eager=True,
|
|
112
|
+
help="Show version and exit",
|
|
113
|
+
),
|
|
114
|
+
mode: Optional[str] = typer.Option(
|
|
115
|
+
None,
|
|
116
|
+
"--mode",
|
|
117
|
+
callback=mode_callback,
|
|
118
|
+
help="Operational mode: cicd (fast, deterministic) or copilot (enhanced, interactive)",
|
|
119
|
+
),
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
SpecFact CLI - Spec→Contract→Sentinel for contract-driven development.
|
|
123
|
+
|
|
124
|
+
Transform your development workflow with automated quality gates,
|
|
125
|
+
runtime contract validation, and state machine workflows.
|
|
126
|
+
|
|
127
|
+
Mode Detection:
|
|
128
|
+
- Explicit --mode flag (highest priority)
|
|
129
|
+
- Auto-detect from environment (CoPilot API, IDE integration)
|
|
130
|
+
- Default to CI/CD mode
|
|
131
|
+
"""
|
|
132
|
+
# Store mode in context for commands to access
|
|
133
|
+
if ctx.obj is None:
|
|
134
|
+
ctx.obj = {}
|
|
135
|
+
ctx.obj["mode"] = get_current_mode()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command()
|
|
139
|
+
def hello() -> None:
|
|
140
|
+
"""
|
|
141
|
+
Test command to verify CLI installation.
|
|
142
|
+
"""
|
|
143
|
+
console.print(
|
|
144
|
+
Panel.fit(
|
|
145
|
+
"[bold green]✓[/bold green] SpecFact CLI is installed and working!\n\n"
|
|
146
|
+
f"Version: [cyan]{__version__}[/cyan]\n"
|
|
147
|
+
"Run [bold]specfact --help[/bold] for available commands.",
|
|
148
|
+
title="[bold]Welcome to SpecFact CLI[/bold]",
|
|
149
|
+
border_style="green",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
@beartype
|
|
156
|
+
def install_completion(
|
|
157
|
+
shell: str = typer.Argument(..., help="Shell name: bash, sh, zsh, fish, powershell, pwsh, ps1"),
|
|
158
|
+
path: Optional[Path] = typer.Option(
|
|
159
|
+
None,
|
|
160
|
+
"--path",
|
|
161
|
+
help="Path to shell configuration file (auto-detected if not provided)",
|
|
162
|
+
),
|
|
163
|
+
) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Install shell completion for SpecFact CLI.
|
|
166
|
+
|
|
167
|
+
Supported shells:
|
|
168
|
+
- bash, sh (bash-compatible)
|
|
169
|
+
- zsh
|
|
170
|
+
- fish
|
|
171
|
+
- powershell, pwsh, ps1 (PowerShell)
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
specfact install-completion bash
|
|
175
|
+
specfact install-completion zsh
|
|
176
|
+
specfact install-completion powershell
|
|
177
|
+
"""
|
|
178
|
+
# Normalize shell name
|
|
179
|
+
shell_normalized = shell.lower().strip()
|
|
180
|
+
mapped_shell = SHELL_MAP.get(shell_normalized)
|
|
181
|
+
|
|
182
|
+
if not mapped_shell:
|
|
183
|
+
console.print(f"[bold red]✗[/bold red] Unsupported shell: {shell}")
|
|
184
|
+
console.print(
|
|
185
|
+
f"\n[dim]Supported shells: {', '.join(sorted(set(SHELL_MAP.values())))}, sh (mapped to bash)[/dim]"
|
|
186
|
+
)
|
|
187
|
+
raise typer.Exit(1)
|
|
188
|
+
|
|
189
|
+
# Generate completion script using subprocess to call CLI with completion env var
|
|
190
|
+
try:
|
|
191
|
+
import subprocess
|
|
192
|
+
|
|
193
|
+
if mapped_shell == "powershell":
|
|
194
|
+
# PowerShell completion requires click-pwsh extension
|
|
195
|
+
completion_script = "# PowerShell completion requires click-pwsh extension\n"
|
|
196
|
+
completion_script += "# Install: pip install click-pwsh\n"
|
|
197
|
+
completion_script += "# Then run: python -m click_pwsh install specfact\n"
|
|
198
|
+
else:
|
|
199
|
+
# Use subprocess to get completion script from Typer/Click
|
|
200
|
+
env = os.environ.copy()
|
|
201
|
+
env["_SPECFACT_COMPLETE"] = f"{mapped_shell}_source"
|
|
202
|
+
|
|
203
|
+
# Call the CLI with completion environment variable to get script
|
|
204
|
+
result = subprocess.run(
|
|
205
|
+
[sys.executable, "-m", "specfact_cli.cli"],
|
|
206
|
+
env=env,
|
|
207
|
+
capture_output=True,
|
|
208
|
+
text=True,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if result.returncode == 0 and result.stdout:
|
|
212
|
+
completion_script = result.stdout
|
|
213
|
+
else:
|
|
214
|
+
# Fallback: Provide instructions for manual installation
|
|
215
|
+
completion_script = f"# SpecFact CLI completion for {mapped_shell}\n"
|
|
216
|
+
completion_script += f"# Add to your {mapped_shell} config file:\n"
|
|
217
|
+
completion_script += f'eval "$(_SPECFACT_COMPLETE={mapped_shell}_source specfact)"\n'
|
|
218
|
+
|
|
219
|
+
# Determine config file path if not provided
|
|
220
|
+
if path is None:
|
|
221
|
+
if mapped_shell == "bash":
|
|
222
|
+
path = Path.home() / ".bashrc"
|
|
223
|
+
elif mapped_shell == "zsh":
|
|
224
|
+
path = Path.home() / ".zshrc"
|
|
225
|
+
elif mapped_shell == "fish":
|
|
226
|
+
path = Path.home() / ".config" / "fish" / "config.fish"
|
|
227
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
elif mapped_shell == "powershell":
|
|
229
|
+
# PowerShell profile location
|
|
230
|
+
profile_paths = [
|
|
231
|
+
Path.home() / "Documents" / "PowerShell" / "Microsoft.PowerShell_profile.ps1",
|
|
232
|
+
Path.home() / ".config" / "powershell" / "Microsoft.PowerShell_profile.ps1",
|
|
233
|
+
]
|
|
234
|
+
for profile_path in profile_paths:
|
|
235
|
+
if profile_path.parent.exists() or profile_path.parent.parent.exists():
|
|
236
|
+
path = profile_path
|
|
237
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
break
|
|
239
|
+
else:
|
|
240
|
+
# Default to first option
|
|
241
|
+
path = profile_paths[0]
|
|
242
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
|
|
244
|
+
# Ensure path is not None
|
|
245
|
+
if path is None:
|
|
246
|
+
console.print("[bold red]✗[/bold red] Could not determine shell configuration file path")
|
|
247
|
+
raise typer.Exit(1)
|
|
248
|
+
|
|
249
|
+
# Check if already installed
|
|
250
|
+
if path.exists():
|
|
251
|
+
with path.open(encoding="utf-8") as f:
|
|
252
|
+
content = f.read()
|
|
253
|
+
if "specfact" in content and ("_SPECFACT_COMPLETE" in content or "_SPECFACT" in content):
|
|
254
|
+
console.print(f"[yellow]⚠[/yellow] Completion already installed in {path}")
|
|
255
|
+
console.print("[dim]Remove existing completion and re-run to update.[/dim]")
|
|
256
|
+
raise typer.Exit(0)
|
|
257
|
+
|
|
258
|
+
# Append completion script
|
|
259
|
+
with path.open("a", encoding="utf-8") as f:
|
|
260
|
+
f.write(f"\n# SpecFact CLI completion for {mapped_shell}\n")
|
|
261
|
+
f.write(completion_script)
|
|
262
|
+
if completion_script and not completion_script.endswith("\n"):
|
|
263
|
+
f.write("\n")
|
|
264
|
+
|
|
265
|
+
console.print(f"[bold green]✓[/bold green] Completion installed for {mapped_shell} in {path}")
|
|
266
|
+
if mapped_shell != "powershell":
|
|
267
|
+
console.print(f"[dim]Reload your shell or run: source {path}[/dim]")
|
|
268
|
+
else:
|
|
269
|
+
console.print("[dim]Reload your PowerShell session to enable completion.[/dim]")
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
console.print(f"[bold red]✗[/bold red] Failed to install completion: {e}")
|
|
273
|
+
raise typer.Exit(1) from e
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command()
|
|
277
|
+
@beartype
|
|
278
|
+
def show_completion(
|
|
279
|
+
shell: str = typer.Argument(..., help="Shell name: bash, sh, zsh, fish, powershell, pwsh, ps1"),
|
|
280
|
+
) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Show shell completion script for SpecFact CLI.
|
|
283
|
+
|
|
284
|
+
Supported shells:
|
|
285
|
+
- bash, sh (bash-compatible)
|
|
286
|
+
- zsh
|
|
287
|
+
- fish
|
|
288
|
+
- powershell, pwsh, ps1 (PowerShell)
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
specfact show-completion bash
|
|
292
|
+
specfact show-completion zsh
|
|
293
|
+
"""
|
|
294
|
+
# Normalize shell name
|
|
295
|
+
shell_normalized = shell.lower().strip()
|
|
296
|
+
mapped_shell = SHELL_MAP.get(shell_normalized)
|
|
297
|
+
|
|
298
|
+
if not mapped_shell:
|
|
299
|
+
console.print(f"[bold red]✗[/bold red] Unsupported shell: {shell}")
|
|
300
|
+
console.print(
|
|
301
|
+
f"\n[dim]Supported shells: {', '.join(sorted(set(SHELL_MAP.values())))}, sh (mapped to bash)[/dim]"
|
|
302
|
+
)
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
|
|
305
|
+
# Generate completion script using subprocess to call CLI with completion env var
|
|
306
|
+
try:
|
|
307
|
+
import subprocess
|
|
308
|
+
|
|
309
|
+
if mapped_shell == "powershell":
|
|
310
|
+
# PowerShell completion requires click-pwsh extension
|
|
311
|
+
completion_script = "# PowerShell completion requires click-pwsh extension\n"
|
|
312
|
+
completion_script += "# Install: pip install click-pwsh\n"
|
|
313
|
+
completion_script += "# Then run: python -m click_pwsh install specfact\n"
|
|
314
|
+
else:
|
|
315
|
+
# Use subprocess to get completion script from Typer/Click
|
|
316
|
+
# Normalize shell name in subprocess call
|
|
317
|
+
env = os.environ.copy()
|
|
318
|
+
env["_SPECFACT_COMPLETE"] = f"{mapped_shell}_source"
|
|
319
|
+
|
|
320
|
+
# Call the CLI with completion environment variable to get script
|
|
321
|
+
# Note: We need to bypass our own command and use Typer's built-in
|
|
322
|
+
result = subprocess.run(
|
|
323
|
+
[sys.executable, "-m", "specfact_cli.cli"],
|
|
324
|
+
env=env,
|
|
325
|
+
capture_output=True,
|
|
326
|
+
text=True,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if result.returncode == 0 and result.stdout and result.stdout.strip():
|
|
330
|
+
completion_script = result.stdout
|
|
331
|
+
else:
|
|
332
|
+
# Fallback: Provide instructions for manual installation
|
|
333
|
+
completion_script = f"# SpecFact CLI completion for {mapped_shell}\n"
|
|
334
|
+
completion_script += f"# Add to your {mapped_shell} config file:\n"
|
|
335
|
+
completion_script += f'eval "$(_SPECFACT_COMPLETE={mapped_shell}_source specfact)"\n'
|
|
336
|
+
|
|
337
|
+
print(completion_script)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
console.print(f"[bold red]✗[/bold red] Failed to generate completion script: {e}")
|
|
341
|
+
raise typer.Exit(1) from e
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# Register command groups
|
|
345
|
+
app.add_typer(import_cmd.app, name="import", help="Import codebases and Spec-Kit projects")
|
|
346
|
+
app.add_typer(plan.app, name="plan", help="Manage development plans")
|
|
347
|
+
app.add_typer(enforce.app, name="enforce", help="Configure quality gates")
|
|
348
|
+
app.add_typer(repro.app, name="repro", help="Run validation suite")
|
|
349
|
+
app.add_typer(sync.app, name="sync", help="Synchronize Spec-Kit artifacts and repository changes")
|
|
350
|
+
app.add_typer(init.app, name="init", help="Initialize SpecFact for IDE integration")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def cli_main() -> None:
|
|
354
|
+
"""Entry point for the CLI application."""
|
|
355
|
+
# Intercept completion environment variable and normalize shell names
|
|
356
|
+
# (This handles completion scripts generated by our custom commands)
|
|
357
|
+
completion_env = os.environ.get("_SPECFACT_COMPLETE")
|
|
358
|
+
if completion_env:
|
|
359
|
+
# Extract shell name from completion env var (format: "shell_source" or "shell")
|
|
360
|
+
if completion_env.endswith("_source"):
|
|
361
|
+
shell_name = completion_env[:-7] # Remove "_source" suffix
|
|
362
|
+
else:
|
|
363
|
+
shell_name = completion_env
|
|
364
|
+
|
|
365
|
+
# Normalize shell name using our mapping
|
|
366
|
+
shell_normalized = shell_name.lower().strip()
|
|
367
|
+
mapped_shell = SHELL_MAP.get(shell_normalized, shell_normalized)
|
|
368
|
+
|
|
369
|
+
# Update environment variable with normalized shell name
|
|
370
|
+
if mapped_shell != shell_normalized:
|
|
371
|
+
if completion_env.endswith("_source"):
|
|
372
|
+
os.environ["_SPECFACT_COMPLETE"] = f"{mapped_shell}_source"
|
|
373
|
+
else:
|
|
374
|
+
os.environ["_SPECFACT_COMPLETE"] = mapped_shell
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
app()
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
console.print("\n[yellow]Operation cancelled by user[/yellow]")
|
|
380
|
+
sys.exit(130)
|
|
381
|
+
except ViolationError as e:
|
|
382
|
+
# Extract user-friendly error message from ViolationError
|
|
383
|
+
error_msg = str(e)
|
|
384
|
+
# Try to extract the contract message (after ":\n")
|
|
385
|
+
if ":\n" in error_msg:
|
|
386
|
+
contract_msg = error_msg.split(":\n", 1)[0]
|
|
387
|
+
console.print(f"[bold red]✗[/bold red] {contract_msg}", style="red")
|
|
388
|
+
else:
|
|
389
|
+
console.print(f"[bold red]✗[/bold red] {error_msg}", style="red")
|
|
390
|
+
sys.exit(1)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
console.print(f"[bold red]Error:[/bold red] {e}", style="red")
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
if __name__ == "__main__":
|
|
397
|
+
cli_main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enforce command - Configure contract validation quality gates.
|
|
3
|
+
|
|
4
|
+
This module provides commands for configuring enforcement modes
|
|
5
|
+
and validation policies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from beartype import beartype
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from specfact_cli.models.enforcement import EnforcementConfig, EnforcementPreset
|
|
16
|
+
from specfact_cli.utils.structure import SpecFactStructure
|
|
17
|
+
from specfact_cli.utils.yaml_utils import dump_yaml
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Configure quality gates and enforcement modes")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("stage")
|
|
24
|
+
@beartype
|
|
25
|
+
def stage(
|
|
26
|
+
preset: str = typer.Option(
|
|
27
|
+
"balanced",
|
|
28
|
+
"--preset",
|
|
29
|
+
help="Enforcement preset (minimal, balanced, strict)",
|
|
30
|
+
),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Set enforcement mode for contract validation.
|
|
34
|
+
|
|
35
|
+
Modes:
|
|
36
|
+
- minimal: Log violations, never block
|
|
37
|
+
- balanced: Block HIGH severity, warn MEDIUM
|
|
38
|
+
- strict: Block all MEDIUM+ violations
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
specfact enforce stage --preset balanced
|
|
42
|
+
"""
|
|
43
|
+
# Validate preset (contract-style validation)
|
|
44
|
+
if not isinstance(preset, str) or len(preset) == 0:
|
|
45
|
+
console.print("[bold red]✗[/bold red] Preset must be non-empty string")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
if preset.lower() not in ("minimal", "balanced", "strict"):
|
|
49
|
+
console.print(f"[bold red]✗[/bold red] Unknown preset: {preset}")
|
|
50
|
+
console.print("Valid presets: minimal, balanced, strict")
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
console.print(f"[bold cyan]Setting enforcement mode:[/bold cyan] {preset}")
|
|
54
|
+
|
|
55
|
+
# Validate preset enum
|
|
56
|
+
try:
|
|
57
|
+
preset_enum = EnforcementPreset(preset)
|
|
58
|
+
except ValueError:
|
|
59
|
+
console.print(f"[bold red]✗[/bold red] Unknown preset: {preset}")
|
|
60
|
+
console.print("Valid presets: minimal, balanced, strict")
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
# Create enforcement configuration
|
|
64
|
+
config = EnforcementConfig.from_preset(preset_enum)
|
|
65
|
+
|
|
66
|
+
# Display configuration as table
|
|
67
|
+
table = Table(title=f"Enforcement Mode: {preset.upper()}")
|
|
68
|
+
table.add_column("Severity", style="cyan")
|
|
69
|
+
table.add_column("Action", style="yellow")
|
|
70
|
+
|
|
71
|
+
for severity, action in config.to_summary_dict().items():
|
|
72
|
+
table.add_row(severity, action)
|
|
73
|
+
|
|
74
|
+
console.print(table)
|
|
75
|
+
|
|
76
|
+
# Ensure .specfact structure exists
|
|
77
|
+
SpecFactStructure.ensure_structure()
|
|
78
|
+
|
|
79
|
+
# Write configuration to file
|
|
80
|
+
config_path = SpecFactStructure.get_enforcement_config_path()
|
|
81
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
# Use mode='json' to convert enums to their string values
|
|
84
|
+
dump_yaml(config.model_dump(mode="json"), config_path)
|
|
85
|
+
|
|
86
|
+
console.print(f"\n[bold green]✓[/bold green] Enforcement mode set to {preset}")
|
|
87
|
+
console.print(f"[dim]Configuration saved to: {config_path}[/dim]")
|