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.

Files changed (60) hide show
  1. specfact_cli/__init__.py +14 -0
  2. specfact_cli/agents/__init__.py +23 -0
  3. specfact_cli/agents/analyze_agent.py +392 -0
  4. specfact_cli/agents/base.py +95 -0
  5. specfact_cli/agents/plan_agent.py +202 -0
  6. specfact_cli/agents/registry.py +176 -0
  7. specfact_cli/agents/sync_agent.py +133 -0
  8. specfact_cli/analyzers/__init__.py +10 -0
  9. specfact_cli/analyzers/code_analyzer.py +775 -0
  10. specfact_cli/cli.py +397 -0
  11. specfact_cli/commands/__init__.py +7 -0
  12. specfact_cli/commands/enforce.py +87 -0
  13. specfact_cli/commands/import_cmd.py +355 -0
  14. specfact_cli/commands/init.py +119 -0
  15. specfact_cli/commands/plan.py +1090 -0
  16. specfact_cli/commands/repro.py +172 -0
  17. specfact_cli/commands/sync.py +408 -0
  18. specfact_cli/common/__init__.py +24 -0
  19. specfact_cli/common/logger_setup.py +673 -0
  20. specfact_cli/common/logging_utils.py +41 -0
  21. specfact_cli/common/text_utils.py +52 -0
  22. specfact_cli/common/utils.py +48 -0
  23. specfact_cli/comparators/__init__.py +10 -0
  24. specfact_cli/comparators/plan_comparator.py +391 -0
  25. specfact_cli/generators/__init__.py +13 -0
  26. specfact_cli/generators/plan_generator.py +105 -0
  27. specfact_cli/generators/protocol_generator.py +115 -0
  28. specfact_cli/generators/report_generator.py +200 -0
  29. specfact_cli/generators/workflow_generator.py +111 -0
  30. specfact_cli/importers/__init__.py +6 -0
  31. specfact_cli/importers/speckit_converter.py +773 -0
  32. specfact_cli/importers/speckit_scanner.py +704 -0
  33. specfact_cli/models/__init__.py +32 -0
  34. specfact_cli/models/deviation.py +105 -0
  35. specfact_cli/models/enforcement.py +150 -0
  36. specfact_cli/models/plan.py +97 -0
  37. specfact_cli/models/protocol.py +28 -0
  38. specfact_cli/modes/__init__.py +18 -0
  39. specfact_cli/modes/detector.py +126 -0
  40. specfact_cli/modes/router.py +153 -0
  41. specfact_cli/sync/__init__.py +11 -0
  42. specfact_cli/sync/repository_sync.py +279 -0
  43. specfact_cli/sync/speckit_sync.py +388 -0
  44. specfact_cli/utils/__init__.py +57 -0
  45. specfact_cli/utils/console.py +69 -0
  46. specfact_cli/utils/feature_keys.py +213 -0
  47. specfact_cli/utils/git.py +241 -0
  48. specfact_cli/utils/ide_setup.py +381 -0
  49. specfact_cli/utils/prompts.py +179 -0
  50. specfact_cli/utils/structure.py +496 -0
  51. specfact_cli/utils/yaml_utils.py +200 -0
  52. specfact_cli/validators/__init__.py +19 -0
  53. specfact_cli/validators/fsm.py +260 -0
  54. specfact_cli/validators/repro_checker.py +320 -0
  55. specfact_cli/validators/schema.py +200 -0
  56. specfact_cli-0.4.0.dist-info/METADATA +332 -0
  57. specfact_cli-0.4.0.dist-info/RECORD +60 -0
  58. specfact_cli-0.4.0.dist-info/WHEEL +4 -0
  59. specfact_cli-0.4.0.dist-info/entry_points.txt +2 -0
  60. 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,7 @@
1
+ """
2
+ SpecFact CLI commands package.
3
+
4
+ This package contains all CLI command implementations.
5
+ """
6
+
7
+ __all__ = []
@@ -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]")