specfact-cli 0.4.2__py3-none-any.whl → 0.6.8__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 (66) hide show
  1. specfact_cli/__init__.py +1 -1
  2. specfact_cli/agents/analyze_agent.py +2 -3
  3. specfact_cli/analyzers/__init__.py +2 -1
  4. specfact_cli/analyzers/ambiguity_scanner.py +601 -0
  5. specfact_cli/analyzers/code_analyzer.py +462 -30
  6. specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
  7. specfact_cli/analyzers/contract_extractor.py +419 -0
  8. specfact_cli/analyzers/control_flow_analyzer.py +281 -0
  9. specfact_cli/analyzers/requirement_extractor.py +337 -0
  10. specfact_cli/analyzers/test_pattern_extractor.py +330 -0
  11. specfact_cli/cli.py +151 -206
  12. specfact_cli/commands/constitution.py +281 -0
  13. specfact_cli/commands/enforce.py +42 -34
  14. specfact_cli/commands/import_cmd.py +481 -152
  15. specfact_cli/commands/init.py +224 -55
  16. specfact_cli/commands/plan.py +2133 -547
  17. specfact_cli/commands/repro.py +100 -78
  18. specfact_cli/commands/sync.py +701 -186
  19. specfact_cli/enrichers/constitution_enricher.py +765 -0
  20. specfact_cli/enrichers/plan_enricher.py +294 -0
  21. specfact_cli/importers/speckit_converter.py +364 -48
  22. specfact_cli/importers/speckit_scanner.py +65 -0
  23. specfact_cli/models/plan.py +42 -0
  24. specfact_cli/resources/mappings/node-async.yaml +49 -0
  25. specfact_cli/resources/mappings/python-async.yaml +47 -0
  26. specfact_cli/resources/mappings/speckit-default.yaml +82 -0
  27. specfact_cli/resources/prompts/specfact-enforce.md +185 -0
  28. specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
  29. specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
  30. specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
  31. specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
  32. specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
  33. specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
  34. specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
  35. specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
  36. specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
  37. specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
  38. specfact_cli/resources/prompts/specfact-repro.md +268 -0
  39. specfact_cli/resources/prompts/specfact-sync.md +497 -0
  40. specfact_cli/resources/schemas/deviation.schema.json +61 -0
  41. specfact_cli/resources/schemas/plan.schema.json +204 -0
  42. specfact_cli/resources/schemas/protocol.schema.json +53 -0
  43. specfact_cli/resources/templates/github-action.yml.j2 +140 -0
  44. specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
  45. specfact_cli/resources/templates/pr-template.md.j2 +58 -0
  46. specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
  47. specfact_cli/resources/templates/telemetry.yaml.example +35 -0
  48. specfact_cli/sync/__init__.py +10 -1
  49. specfact_cli/sync/watcher.py +268 -0
  50. specfact_cli/telemetry.py +440 -0
  51. specfact_cli/utils/acceptance_criteria.py +127 -0
  52. specfact_cli/utils/enrichment_parser.py +445 -0
  53. specfact_cli/utils/feature_keys.py +12 -3
  54. specfact_cli/utils/ide_setup.py +170 -0
  55. specfact_cli/utils/structure.py +179 -2
  56. specfact_cli/utils/yaml_utils.py +33 -0
  57. specfact_cli/validators/repro_checker.py +22 -1
  58. specfact_cli/validators/schema.py +15 -4
  59. specfact_cli-0.6.8.dist-info/METADATA +456 -0
  60. specfact_cli-0.6.8.dist-info/RECORD +99 -0
  61. {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
  62. specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
  63. specfact_cli-0.4.2.dist-info/METADATA +0 -370
  64. specfact_cli-0.4.2.dist-info/RECORD +0 -62
  65. specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
  66. {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/WHEEL +0 -0
specfact_cli/cli.py CHANGED
@@ -8,7 +8,40 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
  import sys
11
- from pathlib import Path
11
+
12
+
13
+ # Patch shellingham before Typer imports it to normalize "sh" to "bash"
14
+ # This fixes auto-detection on Ubuntu where /bin/sh points to dash
15
+ try:
16
+ import shellingham
17
+
18
+ # Store original function
19
+ _original_detect_shell = shellingham.detect_shell
20
+
21
+ def _normalized_detect_shell(pid=None, max_depth=10): # type: ignore[misc]
22
+ """Normalized shell detection that maps 'sh' to 'bash'."""
23
+ shell_name, shell_path = _original_detect_shell(pid, max_depth) # type: ignore[misc]
24
+ if shell_name:
25
+ shell_lower = shell_name.lower()
26
+ # Map shell names using our normalization
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",
34
+ "ps1": "powershell",
35
+ }
36
+ normalized = shell_map.get(shell_lower, shell_lower)
37
+ return (normalized, shell_path)
38
+ return (shell_name, shell_path)
39
+
40
+ # Patch shellingham's detect_shell function
41
+ shellingham.detect_shell = _normalized_detect_shell
42
+ except ImportError:
43
+ # shellingham not available, will use fallback logic
44
+ pass
12
45
 
13
46
  import typer
14
47
  from beartype import beartype
@@ -19,7 +52,7 @@ from rich.panel import Panel
19
52
  from specfact_cli import __version__
20
53
 
21
54
  # Import command modules
22
- from specfact_cli.commands import enforce, import_cmd, init, plan, repro, sync
55
+ from specfact_cli.commands import constitution, enforce, import_cmd, init, plan, repro, sync
23
56
  from specfact_cli.modes import OperationalMode, detect_mode
24
57
 
25
58
 
@@ -36,14 +69,27 @@ SHELL_MAP = {
36
69
 
37
70
 
38
71
  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
72
+ """Normalize shell names in sys.argv before Typer processes them.
73
+
74
+ Also handles auto-detection case where Typer detects "sh" instead of "bash".
75
+ """
76
+ if len(sys.argv) >= 2 and sys.argv[1] in ("--show-completion", "--install-completion"):
77
+ # If shell is provided as argument, normalize it
78
+ if len(sys.argv) >= 3:
79
+ shell_arg = sys.argv[2]
80
+ shell_normalized = shell_arg.lower().strip()
81
+ mapped_shell = SHELL_MAP.get(shell_normalized, shell_normalized)
82
+ if mapped_shell != shell_normalized:
83
+ # Replace "sh" with "bash" in argv (or other mapped shells)
84
+ sys.argv[2] = mapped_shell
85
+ else:
86
+ # Auto-detection case: Typer will detect shell, but we need to ensure
87
+ # it doesn't detect "sh". We'll intercept after Typer detects it.
88
+ # For now, explicitly pass "bash" if SHELL env var points to sh/bash
89
+ shell_env = os.environ.get("SHELL", "")
90
+ if shell_env and ("sh" in shell_env.lower() or "bash" in shell_env.lower()):
91
+ # Force bash if shell is sh or bash
92
+ sys.argv.append("bash")
47
93
 
48
94
 
49
95
  # Note: Shell normalization happens in cli_main() before app() is called
@@ -53,8 +99,9 @@ def normalize_shell_in_argv() -> None:
53
99
  app = typer.Typer(
54
100
  name="specfact",
55
101
  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)
102
+ add_completion=True, # Enable Typer's built-in completion (works natively for bash/zsh/fish without extensions)
57
103
  rich_markup_mode="rich",
104
+ context_settings={"help_option_names": ["-h", "--help"]}, # Add -h as alias for --help
58
105
  )
59
106
 
60
107
  console = Console()
@@ -62,6 +109,50 @@ console = Console()
62
109
  # Global mode context (set by --mode flag or auto-detected)
63
110
  _current_mode: OperationalMode | None = None
64
111
 
112
+ # Global banner flag (set by --no-banner flag)
113
+ _show_banner: bool = True
114
+
115
+
116
+ def print_banner() -> None:
117
+ """Print SpecFact CLI ASCII art banner with smooth gradient effect."""
118
+ from rich.text import Text
119
+
120
+ banner_lines = [
121
+ "",
122
+ " ███████╗██████╗ ███████╗ ██████╗███████╗ █████╗ ██████╗████████╗",
123
+ " ██╔════╝██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝╚══██╔══╝",
124
+ " ███████╗██████╔╝█████╗ ██║ █████╗ ███████║██║ ██║ ",
125
+ " ╚════██║██╔═══╝ ██╔══╝ ██║ ██╔══╝ ██╔══██║██║ ██║ ",
126
+ " ███████║██║ ███████╗╚██████╗██║ ██║ ██║╚██████╗ ██║ ",
127
+ " ╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ",
128
+ "",
129
+ " Spec→Contract→Sentinel for Contract-Driven Development",
130
+ ]
131
+
132
+ # Smooth gradient from bright cyan (top) to blue (bottom) - 6 lines for ASCII art
133
+ # Using Rich's gradient colors: bright_cyan → cyan → bright_blue → blue
134
+ gradient_colors = [
135
+ "black", # Empty line
136
+ "blue", # Line 1 - darkest at top
137
+ "blue", # Line 2
138
+ "cyan", # Line 3
139
+ "cyan", # Line 4
140
+ "white", # Line 5
141
+ "white", # Line 6 - lightest at bottom
142
+ ]
143
+
144
+ for i, line in enumerate(banner_lines):
145
+ if line.strip(): # Only apply gradient to non-empty lines
146
+ if i < len(gradient_colors):
147
+ # Apply gradient color to ASCII art lines
148
+ text = Text(line, style=f"bold {gradient_colors[i]}")
149
+ console.print(text)
150
+ else:
151
+ # Tagline in cyan (after empty line)
152
+ console.print(line, style="cyan")
153
+ else:
154
+ console.print() # Empty line
155
+
65
156
 
66
157
  def version_callback(value: bool) -> None:
67
158
  """Show version information."""
@@ -109,6 +200,11 @@ def main(
109
200
  is_eager=True,
110
201
  help="Show version and exit",
111
202
  ),
203
+ no_banner: bool = typer.Option(
204
+ False,
205
+ "--no-banner",
206
+ help="Hide ASCII art banner (useful for CI/CD)",
207
+ ),
112
208
  mode: str | None = typer.Option(
113
209
  None,
114
210
  "--mode",
@@ -127,6 +223,16 @@ def main(
127
223
  - Auto-detect from environment (CoPilot API, IDE integration)
128
224
  - Default to CI/CD mode
129
225
  """
226
+ global _show_banner
227
+ # Set banner flag based on --no-banner option
228
+ _show_banner = not no_banner
229
+
230
+ # Show help if no command provided (avoids user confusion)
231
+ if ctx.invoked_subcommand is None:
232
+ # Show help by calling Typer's help callback
233
+ ctx.get_help()
234
+ raise typer.Exit()
235
+
130
236
  # Store mode in context for commands to access
131
237
  if ctx.obj is None:
132
238
  ctx.obj = {}
@@ -149,201 +255,12 @@ def hello() -> None:
149
255
  )
150
256
 
151
257
 
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
258
  # Register command groups
259
+ app.add_typer(
260
+ constitution.app,
261
+ name="constitution",
262
+ help="Manage project constitutions (Spec-Kit compatibility layer)",
263
+ )
347
264
  app.add_typer(import_cmd.app, name="import", help="Import codebases and Spec-Kit projects")
348
265
  app.add_typer(plan.app, name="plan", help="Manage development plans")
349
266
  app.add_typer(enforce.app, name="enforce", help="Configure quality gates")
@@ -354,8 +271,30 @@ app.add_typer(init.app, name="init", help="Initialize SpecFact for IDE integrati
354
271
 
355
272
  def cli_main() -> None:
356
273
  """Entry point for the CLI application."""
274
+ # Normalize shell names in argv for Typer's built-in completion commands
275
+ normalize_shell_in_argv()
276
+
277
+ # Check if --no-banner flag is present (before Typer processes it)
278
+ no_banner_requested = "--no-banner" in sys.argv
279
+
280
+ # Show banner by default unless --no-banner is specified
281
+ # Banner shows for: no args, --help/-h, or any command (unless --no-banner)
282
+ show_banner = not no_banner_requested
283
+
284
+ # Intercept Typer's shell detection for --show-completion and --install-completion
285
+ # when no shell is provided (auto-detection case)
286
+ # On Ubuntu, shellingham detects "sh" (dash) instead of "bash", so we force "bash"
287
+ if len(sys.argv) >= 2 and sys.argv[1] in ("--show-completion", "--install-completion") and len(sys.argv) == 2:
288
+ # Auto-detection case: Typer will use shellingham to detect shell
289
+ # On Ubuntu, this often detects "sh" (dash) instead of "bash"
290
+ # Force "bash" if SHELL env var suggests bash/sh to avoid "sh not supported" error
291
+ shell_env = os.environ.get("SHELL", "").lower()
292
+ if "sh" in shell_env or "bash" in shell_env:
293
+ # Force bash by adding it to argv before Typer's auto-detection runs
294
+ sys.argv.append("bash")
295
+
357
296
  # Intercept completion environment variable and normalize shell names
358
- # (This handles completion scripts generated by our custom commands)
297
+ # (This handles completion scripts generated by Typer's built-in commands)
359
298
  completion_env = os.environ.get("_SPECFACT_COMPLETE")
360
299
  if completion_env:
361
300
  # Extract shell name from completion env var (format: "shell_source" or "shell")
@@ -372,6 +311,12 @@ def cli_main() -> None:
372
311
  else:
373
312
  os.environ["_SPECFACT_COMPLETE"] = mapped_shell
374
313
 
314
+ # Show banner by default (unless --no-banner is specified)
315
+ # Only show once, before Typer processes the command
316
+ if show_banner:
317
+ print_banner()
318
+ console.print() # Empty line after banner
319
+
375
320
  try:
376
321
  app()
377
322
  except KeyboardInterrupt: