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