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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Import command - Import codebases and Spec-Kit projects to contract-driven format.
|
|
3
|
+
|
|
4
|
+
This module provides commands for importing existing codebases (brownfield) and
|
|
5
|
+
Spec-Kit projects and converting them to SpecFact contract-driven format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from beartype import beartype
|
|
15
|
+
from icontract import ensure, require
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Import codebases and Spec-Kit projects to contract format")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("from-spec-kit")
|
|
24
|
+
def from_spec_kit(
|
|
25
|
+
repo: Path = typer.Option(
|
|
26
|
+
Path("."),
|
|
27
|
+
"--repo",
|
|
28
|
+
help="Path to Spec-Kit repository",
|
|
29
|
+
exists=True,
|
|
30
|
+
file_okay=False,
|
|
31
|
+
dir_okay=True,
|
|
32
|
+
),
|
|
33
|
+
dry_run: bool = typer.Option(
|
|
34
|
+
False,
|
|
35
|
+
"--dry-run",
|
|
36
|
+
help="Preview changes without writing files",
|
|
37
|
+
),
|
|
38
|
+
write: bool = typer.Option(
|
|
39
|
+
False,
|
|
40
|
+
"--write",
|
|
41
|
+
help="Write changes to disk",
|
|
42
|
+
),
|
|
43
|
+
out_branch: str = typer.Option(
|
|
44
|
+
"feat/specfact-migration",
|
|
45
|
+
"--out-branch",
|
|
46
|
+
help="Feature branch name for migration",
|
|
47
|
+
),
|
|
48
|
+
report: Optional[Path] = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
"--report",
|
|
51
|
+
help="Path to write import report",
|
|
52
|
+
),
|
|
53
|
+
force: bool = typer.Option(
|
|
54
|
+
False,
|
|
55
|
+
"--force",
|
|
56
|
+
help="Overwrite existing files",
|
|
57
|
+
),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Convert Spec-Kit project to SpecFact contract format.
|
|
61
|
+
|
|
62
|
+
This command scans a Spec-Kit repository, parses its structure,
|
|
63
|
+
and generates equivalent SpecFact contracts, protocols, and plans.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
specfact import from-spec-kit --repo ./my-project --write
|
|
67
|
+
"""
|
|
68
|
+
from specfact_cli.importers.speckit_converter import SpecKitConverter
|
|
69
|
+
from specfact_cli.importers.speckit_scanner import SpecKitScanner
|
|
70
|
+
from specfact_cli.utils.structure import SpecFactStructure
|
|
71
|
+
|
|
72
|
+
console.print(f"[bold cyan]Importing Spec-Kit project from:[/bold cyan] {repo}")
|
|
73
|
+
|
|
74
|
+
# Scan Spec-Kit structure
|
|
75
|
+
scanner = SpecKitScanner(repo)
|
|
76
|
+
|
|
77
|
+
if not scanner.is_speckit_repo():
|
|
78
|
+
console.print("[bold red]✗[/bold red] Not a Spec-Kit repository")
|
|
79
|
+
console.print("[dim]Expected: .specify/ directory[/dim]")
|
|
80
|
+
raise typer.Exit(1)
|
|
81
|
+
|
|
82
|
+
structure = scanner.scan_structure()
|
|
83
|
+
|
|
84
|
+
if dry_run:
|
|
85
|
+
console.print("[yellow]→ Dry run mode - no files will be written[/yellow]")
|
|
86
|
+
console.print("\n[bold]Detected Structure:[/bold]")
|
|
87
|
+
console.print(f" - Specs Directory: {structure.get('specs_dir', 'Not found')}")
|
|
88
|
+
console.print(f" - Memory Directory: {structure.get('specify_memory_dir', 'Not found')}")
|
|
89
|
+
if structure.get("feature_dirs"):
|
|
90
|
+
console.print(f" - Features Found: {len(structure['feature_dirs'])}")
|
|
91
|
+
if structure.get("memory_files"):
|
|
92
|
+
console.print(f" - Memory Files: {len(structure['memory_files'])}")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if not write:
|
|
96
|
+
console.print("[yellow]→ Use --write to actually convert files[/yellow]")
|
|
97
|
+
console.print("[dim]Use --dry-run to preview changes[/dim]")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Ensure SpecFact structure exists
|
|
101
|
+
SpecFactStructure.ensure_structure(repo)
|
|
102
|
+
|
|
103
|
+
with Progress(
|
|
104
|
+
SpinnerColumn(),
|
|
105
|
+
TextColumn("[progress.description]{task.description}"),
|
|
106
|
+
console=console,
|
|
107
|
+
) as progress:
|
|
108
|
+
# Step 1: Discover features from markdown artifacts
|
|
109
|
+
task = progress.add_task("Discovering Spec-Kit features...", total=None)
|
|
110
|
+
features = scanner.discover_features()
|
|
111
|
+
if not features:
|
|
112
|
+
console.print("[bold red]✗[/bold red] No features found in Spec-Kit repository")
|
|
113
|
+
console.print("[dim]Expected: specs/*/spec.md files[/dim]")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
progress.update(task, description=f"✓ Discovered {len(features)} features")
|
|
116
|
+
|
|
117
|
+
# Step 2: Convert protocol
|
|
118
|
+
task = progress.add_task("Converting protocol...", total=None)
|
|
119
|
+
converter = SpecKitConverter(repo)
|
|
120
|
+
protocol = None
|
|
121
|
+
plan_bundle = None
|
|
122
|
+
try:
|
|
123
|
+
protocol = converter.convert_protocol()
|
|
124
|
+
progress.update(task, description=f"✓ Protocol converted ({len(protocol.states)} states)")
|
|
125
|
+
|
|
126
|
+
# Step 3: Convert plan
|
|
127
|
+
task = progress.add_task("Converting plan bundle...", total=None)
|
|
128
|
+
plan_bundle = converter.convert_plan()
|
|
129
|
+
progress.update(task, description=f"✓ Plan converted ({len(plan_bundle.features)} features)")
|
|
130
|
+
|
|
131
|
+
# Step 4: Generate Semgrep rules
|
|
132
|
+
task = progress.add_task("Generating Semgrep rules...", total=None)
|
|
133
|
+
semgrep_path = converter.generate_semgrep_rules()
|
|
134
|
+
progress.update(task, description="✓ Semgrep rules generated")
|
|
135
|
+
|
|
136
|
+
# Step 5: Generate GitHub Action workflow
|
|
137
|
+
task = progress.add_task("Generating GitHub Action workflow...", total=None)
|
|
138
|
+
repo_name = repo.name if isinstance(repo, Path) else None
|
|
139
|
+
workflow_path = converter.generate_github_action(repo_name=repo_name)
|
|
140
|
+
progress.update(task, description="✓ GitHub Action workflow generated")
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
console.print(f"[bold red]✗[/bold red] Conversion failed: {e}")
|
|
144
|
+
raise typer.Exit(1) from e
|
|
145
|
+
|
|
146
|
+
# Generate report
|
|
147
|
+
if report and protocol and plan_bundle:
|
|
148
|
+
report_content = f"""# Spec-Kit Import Report
|
|
149
|
+
|
|
150
|
+
## Repository: {repo}
|
|
151
|
+
|
|
152
|
+
## Summary
|
|
153
|
+
- **States Found**: {len(protocol.states)}
|
|
154
|
+
- **Transitions**: {len(protocol.transitions)}
|
|
155
|
+
- **Features Extracted**: {len(plan_bundle.features)}
|
|
156
|
+
- **Total Stories**: {sum(len(f.stories) for f in plan_bundle.features)}
|
|
157
|
+
|
|
158
|
+
## Generated Files
|
|
159
|
+
- **Protocol**: `.specfact/protocols/workflow.protocol.yaml`
|
|
160
|
+
- **Plan Bundle**: `.specfact/plans/main.bundle.yaml`
|
|
161
|
+
- **Semgrep Rules**: `.semgrep/async-anti-patterns.yml`
|
|
162
|
+
- **GitHub Action**: `.github/workflows/specfact-gate.yml`
|
|
163
|
+
|
|
164
|
+
## States
|
|
165
|
+
{chr(10).join(f"- {state}" for state in protocol.states)}
|
|
166
|
+
|
|
167
|
+
## Features
|
|
168
|
+
{chr(10).join(f"- {f.title} ({f.key})" for f in plan_bundle.features)}
|
|
169
|
+
"""
|
|
170
|
+
report.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
report.write_text(report_content, encoding="utf-8")
|
|
172
|
+
console.print(f"[dim]Report written to: {report}[/dim]")
|
|
173
|
+
|
|
174
|
+
console.print("[bold green]✓[/bold green] Import complete!")
|
|
175
|
+
console.print("[dim]Protocol: .specfact/protocols/workflow.protocol.yaml[/dim]")
|
|
176
|
+
console.print("[dim]Plan: .specfact/plans/main.bundle.yaml[/dim]")
|
|
177
|
+
console.print("[dim]Semgrep Rules: .semgrep/async-anti-patterns.yml[/dim]")
|
|
178
|
+
console.print("[dim]GitHub Action: .github/workflows/specfact-gate.yml[/dim]")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.command("from-code")
|
|
182
|
+
@require(lambda repo: repo.exists() and repo.is_dir(), "Repo path must exist and be directory")
|
|
183
|
+
@require(lambda confidence: 0.0 <= confidence <= 1.0, "Confidence must be 0.0-1.0")
|
|
184
|
+
@ensure(lambda out: out is None or out.exists(), "Output path must exist if provided")
|
|
185
|
+
@beartype
|
|
186
|
+
def from_code(
|
|
187
|
+
repo: Path = typer.Option(
|
|
188
|
+
Path("."),
|
|
189
|
+
"--repo",
|
|
190
|
+
help="Path to repository to import",
|
|
191
|
+
exists=True,
|
|
192
|
+
file_okay=False,
|
|
193
|
+
dir_okay=True,
|
|
194
|
+
),
|
|
195
|
+
name: Optional[str] = typer.Option(
|
|
196
|
+
None,
|
|
197
|
+
"--name",
|
|
198
|
+
help="Custom plan name (will be sanitized for filesystem, default: 'auto-derived')",
|
|
199
|
+
),
|
|
200
|
+
out: Optional[Path] = typer.Option(
|
|
201
|
+
None,
|
|
202
|
+
"--out",
|
|
203
|
+
help="Output plan bundle path (default: .specfact/plans/<name>-<timestamp>.bundle.yaml)",
|
|
204
|
+
),
|
|
205
|
+
shadow_only: bool = typer.Option(
|
|
206
|
+
False,
|
|
207
|
+
"--shadow-only",
|
|
208
|
+
help="Shadow mode - observe without enforcing",
|
|
209
|
+
),
|
|
210
|
+
report: Optional[Path] = typer.Option(
|
|
211
|
+
None,
|
|
212
|
+
"--report",
|
|
213
|
+
help="Path to write analysis report (default: .specfact/reports/brownfield/analysis-<timestamp>.md)",
|
|
214
|
+
),
|
|
215
|
+
confidence: float = typer.Option(
|
|
216
|
+
0.5,
|
|
217
|
+
"--confidence",
|
|
218
|
+
min=0.0,
|
|
219
|
+
max=1.0,
|
|
220
|
+
help="Minimum confidence score for features",
|
|
221
|
+
),
|
|
222
|
+
key_format: str = typer.Option(
|
|
223
|
+
"classname",
|
|
224
|
+
"--key-format",
|
|
225
|
+
help="Feature key format: 'classname' (FEATURE-CLASSNAME) or 'sequential' (FEATURE-001)",
|
|
226
|
+
),
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Import plan bundle from existing codebase (one-way import).
|
|
230
|
+
|
|
231
|
+
Analyzes code structure using AI-first semantic understanding or AST-based fallback
|
|
232
|
+
to generate a plan bundle that represents the current system.
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
specfact import from-code --repo . --out brownfield-plan.yaml
|
|
236
|
+
"""
|
|
237
|
+
from specfact_cli.agents.analyze_agent import AnalyzeAgent
|
|
238
|
+
from specfact_cli.agents.registry import get_agent
|
|
239
|
+
from specfact_cli.cli import get_current_mode
|
|
240
|
+
from specfact_cli.modes import get_router
|
|
241
|
+
|
|
242
|
+
mode = get_current_mode()
|
|
243
|
+
|
|
244
|
+
# Route command based on mode
|
|
245
|
+
router = get_router()
|
|
246
|
+
routing_result = router.route("import from-code", mode, {"repo": str(repo), "confidence": confidence})
|
|
247
|
+
|
|
248
|
+
from specfact_cli.generators.plan_generator import PlanGenerator
|
|
249
|
+
from specfact_cli.utils.structure import SpecFactStructure
|
|
250
|
+
from specfact_cli.validators.schema import validate_plan_bundle
|
|
251
|
+
|
|
252
|
+
# Ensure .specfact structure exists in the repository being imported
|
|
253
|
+
SpecFactStructure.ensure_structure(repo)
|
|
254
|
+
|
|
255
|
+
# Use default paths if not specified (relative to repo)
|
|
256
|
+
if out is None:
|
|
257
|
+
out = SpecFactStructure.get_timestamped_brownfield_report(repo, name=name)
|
|
258
|
+
|
|
259
|
+
if report is None:
|
|
260
|
+
report = SpecFactStructure.get_brownfield_analysis_path(repo)
|
|
261
|
+
|
|
262
|
+
console.print(f"[bold cyan]Importing repository:[/bold cyan] {repo}")
|
|
263
|
+
console.print(f"[dim]Confidence threshold: {confidence}[/dim]")
|
|
264
|
+
|
|
265
|
+
if shadow_only:
|
|
266
|
+
console.print("[yellow]→ Shadow mode - observe without enforcement[/yellow]")
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Use AI-first approach in CoPilot mode, fallback to AST in CI/CD mode
|
|
270
|
+
if routing_result.execution_mode == "agent":
|
|
271
|
+
console.print("[dim]Mode: CoPilot (AI-first import)[/dim]")
|
|
272
|
+
# Get agent for this command
|
|
273
|
+
agent = get_agent("import from-code")
|
|
274
|
+
if agent and isinstance(agent, AnalyzeAgent):
|
|
275
|
+
# Build context for agent
|
|
276
|
+
context = {
|
|
277
|
+
"workspace": str(repo),
|
|
278
|
+
"current_file": None, # TODO: Get from IDE in Phase 4.2+
|
|
279
|
+
"selection": None, # TODO: Get from IDE in Phase 4.2+
|
|
280
|
+
}
|
|
281
|
+
# Inject context (for future LLM integration)
|
|
282
|
+
_enhanced_context = agent.inject_context(context)
|
|
283
|
+
# Use AI-first import
|
|
284
|
+
console.print("\n[cyan]🤖 AI-powered import (semantic understanding)...[/cyan]")
|
|
285
|
+
plan_bundle = agent.analyze_codebase(repo, confidence=confidence, plan_name=name)
|
|
286
|
+
console.print("[green]✓[/green] AI import complete")
|
|
287
|
+
else:
|
|
288
|
+
# Fallback to AST if agent not available
|
|
289
|
+
console.print("[yellow]⚠ Agent not available, falling back to AST-based import[/yellow]")
|
|
290
|
+
from specfact_cli.analyzers.code_analyzer import CodeAnalyzer
|
|
291
|
+
|
|
292
|
+
console.print("\n[cyan]🔍 Importing Python files (AST-based fallback)...[/cyan]")
|
|
293
|
+
analyzer = CodeAnalyzer(repo, confidence_threshold=confidence, key_format=key_format, plan_name=name)
|
|
294
|
+
plan_bundle = analyzer.analyze()
|
|
295
|
+
else:
|
|
296
|
+
# CI/CD mode: use AST-based import (no LLM available)
|
|
297
|
+
console.print("[dim]Mode: CI/CD (AST-based import)[/dim]")
|
|
298
|
+
from specfact_cli.analyzers.code_analyzer import CodeAnalyzer
|
|
299
|
+
|
|
300
|
+
console.print("\n[cyan]🔍 Importing Python files...[/cyan]")
|
|
301
|
+
analyzer = CodeAnalyzer(repo, confidence_threshold=confidence, key_format=key_format, plan_name=name)
|
|
302
|
+
plan_bundle = analyzer.analyze()
|
|
303
|
+
|
|
304
|
+
console.print(f"[green]✓[/green] Found {len(plan_bundle.features)} features")
|
|
305
|
+
console.print(f"[green]✓[/green] Detected themes: {', '.join(plan_bundle.product.themes)}")
|
|
306
|
+
|
|
307
|
+
# Show summary
|
|
308
|
+
total_stories = sum(len(f.stories) for f in plan_bundle.features)
|
|
309
|
+
console.print(f"[green]✓[/green] Total stories: {total_stories}\n")
|
|
310
|
+
|
|
311
|
+
# Generate plan file
|
|
312
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
generator = PlanGenerator()
|
|
314
|
+
generator.generate(plan_bundle, out)
|
|
315
|
+
|
|
316
|
+
console.print("[bold green]✓ Import complete![/bold green]")
|
|
317
|
+
console.print(f"[dim]Plan bundle written to: {out}[/dim]")
|
|
318
|
+
|
|
319
|
+
# Validate generated plan
|
|
320
|
+
is_valid, error, _ = validate_plan_bundle(out)
|
|
321
|
+
if is_valid:
|
|
322
|
+
console.print("[green]✓ Plan validation passed[/green]")
|
|
323
|
+
else:
|
|
324
|
+
console.print(f"[yellow]⚠ Plan validation warning: {error}[/yellow]")
|
|
325
|
+
|
|
326
|
+
# Generate report
|
|
327
|
+
report_content = f"""# Brownfield Import Report
|
|
328
|
+
|
|
329
|
+
## Repository: {repo}
|
|
330
|
+
|
|
331
|
+
## Summary
|
|
332
|
+
- **Features Found**: {len(plan_bundle.features)}
|
|
333
|
+
- **Total Stories**: {total_stories}
|
|
334
|
+
- **Detected Themes**: {", ".join(plan_bundle.product.themes)}
|
|
335
|
+
- **Confidence Threshold**: {confidence}
|
|
336
|
+
|
|
337
|
+
## Output Files
|
|
338
|
+
- **Plan Bundle**: `{out}`
|
|
339
|
+
- **Import Report**: `{report}`
|
|
340
|
+
|
|
341
|
+
## Features
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
for feature in plan_bundle.features:
|
|
345
|
+
report_content += f"### {feature.title} ({feature.key})\n"
|
|
346
|
+
report_content += f"- **Stories**: {len(feature.stories)}\n"
|
|
347
|
+
report_content += f"- **Confidence**: {feature.confidence}\n"
|
|
348
|
+
report_content += f"- **Outcomes**: {', '.join(feature.outcomes)}\n\n"
|
|
349
|
+
|
|
350
|
+
report.write_text(report_content)
|
|
351
|
+
console.print(f"[dim]Report written to: {report}[/dim]")
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
console.print(f"[bold red]✗ Import failed:[/bold red] {e}")
|
|
355
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Init command - Initialize SpecFact for IDE integration.
|
|
3
|
+
|
|
4
|
+
This module provides the `specfact init` command to copy prompt templates
|
|
5
|
+
to IDE-specific locations for slash command integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from beartype import beartype
|
|
14
|
+
from icontract import ensure, require
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
|
|
18
|
+
from specfact_cli.utils.ide_setup import IDE_CONFIG, copy_templates_to_ide, detect_ide
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(help="Initialize SpecFact for IDE integration")
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback(invoke_without_command=True)
|
|
25
|
+
@require(lambda ide: ide in IDE_CONFIG or ide == "auto", "IDE must be valid or 'auto'")
|
|
26
|
+
@require(lambda repo: repo.exists() and repo.is_dir(), "Repo path must exist and be directory")
|
|
27
|
+
@ensure(lambda result: result is None, "Command should return None")
|
|
28
|
+
@beartype
|
|
29
|
+
def init(
|
|
30
|
+
ide: str = typer.Option(
|
|
31
|
+
"auto",
|
|
32
|
+
"--ide",
|
|
33
|
+
help="IDE type (auto, cursor, vscode, copilot, claude, gemini, qwen, opencode, windsurf, kilocode, auggie, roo, codebuddy, amp, q)",
|
|
34
|
+
),
|
|
35
|
+
repo: Path = typer.Option(
|
|
36
|
+
Path("."),
|
|
37
|
+
"--repo",
|
|
38
|
+
help="Repository path (default: current directory)",
|
|
39
|
+
exists=True,
|
|
40
|
+
file_okay=False,
|
|
41
|
+
dir_okay=True,
|
|
42
|
+
),
|
|
43
|
+
force: bool = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
"--force",
|
|
46
|
+
help="Overwrite existing files",
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Initialize SpecFact for IDE integration.
|
|
51
|
+
|
|
52
|
+
Copies prompt templates to IDE-specific locations so slash commands work.
|
|
53
|
+
This command detects the IDE type (or uses --ide flag) and copies
|
|
54
|
+
SpecFact prompt templates to the appropriate directory.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
specfact init # Auto-detect IDE
|
|
58
|
+
specfact init --ide cursor # Initialize for Cursor
|
|
59
|
+
specfact init --ide vscode --force # Overwrite existing files
|
|
60
|
+
specfact init --repo /path/to/repo --ide copilot
|
|
61
|
+
"""
|
|
62
|
+
# Resolve repo path
|
|
63
|
+
repo_path = repo.resolve()
|
|
64
|
+
|
|
65
|
+
# Detect IDE
|
|
66
|
+
detected_ide = detect_ide(ide)
|
|
67
|
+
ide_config = IDE_CONFIG[detected_ide]
|
|
68
|
+
ide_name = ide_config["name"]
|
|
69
|
+
|
|
70
|
+
console.print()
|
|
71
|
+
console.print(Panel("[bold cyan]SpecFact IDE Setup[/bold cyan]", border_style="cyan"))
|
|
72
|
+
console.print(f"[cyan]Repository:[/cyan] {repo_path}")
|
|
73
|
+
console.print(f"[cyan]IDE:[/cyan] {ide_name} ({detected_ide})")
|
|
74
|
+
console.print()
|
|
75
|
+
|
|
76
|
+
# Find templates directory
|
|
77
|
+
# Try relative to project root first (for development)
|
|
78
|
+
templates_dir = repo_path / "resources" / "prompts"
|
|
79
|
+
if not templates_dir.exists():
|
|
80
|
+
# Try relative to installed package (for distribution)
|
|
81
|
+
import importlib.util
|
|
82
|
+
|
|
83
|
+
spec = importlib.util.find_spec("specfact_cli")
|
|
84
|
+
if spec and spec.origin:
|
|
85
|
+
package_dir = Path(spec.origin).parent.parent
|
|
86
|
+
templates_dir = package_dir / "resources" / "prompts"
|
|
87
|
+
if not templates_dir.exists():
|
|
88
|
+
# Fallback: try resources/prompts in project root
|
|
89
|
+
templates_dir = Path(__file__).parent.parent.parent.parent / "resources" / "prompts"
|
|
90
|
+
|
|
91
|
+
if not templates_dir.exists():
|
|
92
|
+
console.print(f"[red]Error:[/red] Templates directory not found: {templates_dir}")
|
|
93
|
+
console.print("[yellow]Expected location:[/yellow] resources/prompts/")
|
|
94
|
+
console.print("[yellow]Please ensure SpecFact is properly installed.[/yellow]")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
|
|
97
|
+
console.print(f"[cyan]Templates:[/cyan] {templates_dir}")
|
|
98
|
+
console.print()
|
|
99
|
+
|
|
100
|
+
# Copy templates to IDE location
|
|
101
|
+
try:
|
|
102
|
+
copied_files, settings_path = copy_templates_to_ide(repo_path, detected_ide, templates_dir, force)
|
|
103
|
+
|
|
104
|
+
if not copied_files:
|
|
105
|
+
console.print("[yellow]No templates copied (all files already exist, use --force to overwrite)[/yellow]")
|
|
106
|
+
raise typer.Exit(0)
|
|
107
|
+
|
|
108
|
+
console.print()
|
|
109
|
+
console.print(Panel("[bold green]✓ Initialization Complete[/bold green]", border_style="green"))
|
|
110
|
+
console.print(f"[green]Copied {len(copied_files)} template(s) to {ide_config['folder']}[/green]")
|
|
111
|
+
if settings_path:
|
|
112
|
+
console.print(f"[green]Updated VS Code settings:[/green] {settings_path}")
|
|
113
|
+
console.print()
|
|
114
|
+
console.print("[dim]You can now use SpecFact slash commands in your IDE![/dim]")
|
|
115
|
+
console.print("[dim]Example: /specfact-import-from-code --repo . --confidence 0.7[/dim]")
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
console.print(f"[red]Error:[/red] Failed to initialize IDE integration: {e}")
|
|
119
|
+
raise typer.Exit(1) from e
|