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.
- specfact_cli/__init__.py +1 -1
- specfact_cli/agents/analyze_agent.py +2 -3
- specfact_cli/analyzers/__init__.py +2 -1
- specfact_cli/analyzers/ambiguity_scanner.py +601 -0
- specfact_cli/analyzers/code_analyzer.py +462 -30
- specfact_cli/analyzers/constitution_evidence_extractor.py +491 -0
- specfact_cli/analyzers/contract_extractor.py +419 -0
- specfact_cli/analyzers/control_flow_analyzer.py +281 -0
- specfact_cli/analyzers/requirement_extractor.py +337 -0
- specfact_cli/analyzers/test_pattern_extractor.py +330 -0
- specfact_cli/cli.py +151 -206
- specfact_cli/commands/constitution.py +281 -0
- specfact_cli/commands/enforce.py +42 -34
- specfact_cli/commands/import_cmd.py +481 -152
- specfact_cli/commands/init.py +224 -55
- specfact_cli/commands/plan.py +2133 -547
- specfact_cli/commands/repro.py +100 -78
- specfact_cli/commands/sync.py +701 -186
- specfact_cli/enrichers/constitution_enricher.py +765 -0
- specfact_cli/enrichers/plan_enricher.py +294 -0
- specfact_cli/importers/speckit_converter.py +364 -48
- specfact_cli/importers/speckit_scanner.py +65 -0
- specfact_cli/models/plan.py +42 -0
- specfact_cli/resources/mappings/node-async.yaml +49 -0
- specfact_cli/resources/mappings/python-async.yaml +47 -0
- specfact_cli/resources/mappings/speckit-default.yaml +82 -0
- specfact_cli/resources/prompts/specfact-enforce.md +185 -0
- specfact_cli/resources/prompts/specfact-import-from-code.md +626 -0
- specfact_cli/resources/prompts/specfact-plan-add-feature.md +188 -0
- specfact_cli/resources/prompts/specfact-plan-add-story.md +212 -0
- specfact_cli/resources/prompts/specfact-plan-compare.md +571 -0
- specfact_cli/resources/prompts/specfact-plan-init.md +531 -0
- specfact_cli/resources/prompts/specfact-plan-promote.md +352 -0
- specfact_cli/resources/prompts/specfact-plan-review.md +1276 -0
- specfact_cli/resources/prompts/specfact-plan-select.md +401 -0
- specfact_cli/resources/prompts/specfact-plan-update-feature.md +242 -0
- specfact_cli/resources/prompts/specfact-plan-update-idea.md +211 -0
- specfact_cli/resources/prompts/specfact-repro.md +268 -0
- specfact_cli/resources/prompts/specfact-sync.md +497 -0
- specfact_cli/resources/schemas/deviation.schema.json +61 -0
- specfact_cli/resources/schemas/plan.schema.json +204 -0
- specfact_cli/resources/schemas/protocol.schema.json +53 -0
- specfact_cli/resources/templates/github-action.yml.j2 +140 -0
- specfact_cli/resources/templates/plan.bundle.yaml.j2 +141 -0
- specfact_cli/resources/templates/pr-template.md.j2 +58 -0
- specfact_cli/resources/templates/protocol.yaml.j2 +24 -0
- specfact_cli/resources/templates/telemetry.yaml.example +35 -0
- specfact_cli/sync/__init__.py +10 -1
- specfact_cli/sync/watcher.py +268 -0
- specfact_cli/telemetry.py +440 -0
- specfact_cli/utils/acceptance_criteria.py +127 -0
- specfact_cli/utils/enrichment_parser.py +445 -0
- specfact_cli/utils/feature_keys.py +12 -3
- specfact_cli/utils/ide_setup.py +170 -0
- specfact_cli/utils/structure.py +179 -2
- specfact_cli/utils/yaml_utils.py +33 -0
- specfact_cli/validators/repro_checker.py +22 -1
- specfact_cli/validators/schema.py +15 -4
- specfact_cli-0.6.8.dist-info/METADATA +456 -0
- specfact_cli-0.6.8.dist-info/RECORD +99 -0
- {specfact_cli-0.4.2.dist-info → specfact_cli-0.6.8.dist-info}/entry_points.txt +1 -0
- specfact_cli-0.6.8.dist-info/licenses/LICENSE.md +202 -0
- specfact_cli-0.4.2.dist-info/METADATA +0 -370
- specfact_cli-0.4.2.dist-info/RECORD +0 -62
- specfact_cli-0.4.2.dist-info/licenses/LICENSE.md +0 -61
- {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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sys.argv[2]
|
|
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=
|
|
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
|
|
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:
|