specfact-cli 0.6.4__tar.gz → 0.6.6__tar.gz
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-0.6.4 → specfact_cli-0.6.6}/PKG-INFO +1 -1
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/pyproject.toml +2 -1
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-import-from-code.md +13 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-review.md +4 -1
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-update-feature.md +12 -4
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/__init__.py +1 -1
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/cli.py +7 -0
- specfact_cli-0.6.6/src/specfact_cli/commands/init.py +294 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/plan.py +5 -1
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/importers/speckit_converter.py +20 -5
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/ide_setup.py +163 -0
- specfact_cli-0.6.4/src/specfact_cli/commands/init.py +0 -143
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/.gitignore +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/LICENSE.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/README.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-enforce.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-add-feature.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-add-story.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-compare.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-init.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-promote.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-select.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-plan-update-idea.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-repro.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/prompts/specfact-sync.md +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/constitution.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/sync/speckit_sync.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/validators/schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specfact-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.6
|
|
4
4
|
Summary: Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfact-cli"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.6"
|
|
8
8
|
description = "Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -106,6 +106,7 @@ Trademarks = "https://github.com/nold-ai/specfact-cli/blob/main/TRADEMARKS.md"
|
|
|
106
106
|
|
|
107
107
|
[project.scripts]
|
|
108
108
|
specfact = "specfact_cli.cli:cli_main"
|
|
109
|
+
specfact-cli = "specfact_cli.cli:cli_main" # Alias for uvx compatibility
|
|
109
110
|
|
|
110
111
|
# [project.entry-points."pytest11"] # Add if you have pytest plugins
|
|
111
112
|
# specfact_test_plugin = "specfact_cli.pytest_plugin"
|
|
@@ -134,7 +134,11 @@ When in copilot mode, follow this three-phase workflow:
|
|
|
134
134
|
**ALWAYS execute CLI first** to get structured, validated output:
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
+
# Full repository analysis
|
|
137
138
|
specfact import from-code --repo <path> --name <name> --confidence <score>
|
|
139
|
+
|
|
140
|
+
# Partial repository analysis (analyze only specific subdirectory)
|
|
141
|
+
specfact import from-code --repo <path> --name <name> --entry-point <subdirectory> --confidence <score>
|
|
138
142
|
```
|
|
139
143
|
|
|
140
144
|
**Note**: Mode is auto-detected by the CLI (CI/CD in non-interactive environments, CoPilot when in IDE/Copilot session). No need to specify `--mode` flag.
|
|
@@ -245,6 +249,11 @@ Extract arguments from user input:
|
|
|
245
249
|
- `--report PATH` - Analysis report path (optional, default: `.specfact/reports/brownfield/analysis-<timestamp>.md`)
|
|
246
250
|
- `--shadow-only` - Observe mode without enforcing (optional)
|
|
247
251
|
- `--key-format {classname|sequential}` - Feature key format (default: `classname`)
|
|
252
|
+
- `--entry-point PATH` - Subdirectory path for partial analysis (relative to repo root). Analyzes only files within this directory and subdirectories. Useful for:
|
|
253
|
+
- Multi-project repositories (monorepos): Analyze one project at a time
|
|
254
|
+
- Large codebases: Focus on specific modules or subsystems
|
|
255
|
+
- Incremental modernization: Modernize one part of the codebase at a time
|
|
256
|
+
- Example: `--entry-point projects/api-service` analyzes only `projects/api-service/` and its subdirectories
|
|
248
257
|
|
|
249
258
|
**Important**: If `--name` is not provided, **ask the user interactively** for a meaningful plan name and **WAIT for their response**. The name will be automatically sanitized (lowercased, spaces/special chars removed) for filesystem persistence.
|
|
250
259
|
|
|
@@ -261,7 +270,11 @@ For single quotes in args like "I'm Groot", use escape syntax: e.g `'I'\''m Groo
|
|
|
261
270
|
**ALWAYS execute the specfact CLI first** to get structured, validated output:
|
|
262
271
|
|
|
263
272
|
```bash
|
|
273
|
+
# Full repository analysis
|
|
264
274
|
specfact import from-code --repo <repo_path> --name <plan_name> --confidence <confidence>
|
|
275
|
+
|
|
276
|
+
# Partial repository analysis (analyze only specific subdirectory)
|
|
277
|
+
specfact import from-code --repo <repo_path> --name <plan_name> --entry-point <subdirectory> --confidence <confidence>
|
|
265
278
|
```
|
|
266
279
|
|
|
267
280
|
**Note**: Mode is auto-detected by the CLI. No need to specify `--mode` flag.
|
|
@@ -50,7 +50,10 @@ You **MUST** consider the user input before proceeding (if not empty).
|
|
|
50
50
|
|
|
51
51
|
**For updating features**:
|
|
52
52
|
|
|
53
|
-
- `specfact plan update-feature --key <key> --title <title> --outcomes <outcomes> --acceptance <acceptance> --constraints <constraints> --confidence <confidence> --draft
|
|
53
|
+
- `specfact plan update-feature --key <key> --title <title> --outcomes <outcomes> --acceptance <acceptance> --constraints <constraints> --confidence <confidence> --draft/--no-draft --plan <path>`
|
|
54
|
+
- **Boolean flags**: `--draft` sets True, `--no-draft` sets False, omit to leave unchanged
|
|
55
|
+
- ❌ **WRONG**: `--draft true` or `--draft false` (Typer boolean flags don't accept values)
|
|
56
|
+
- ✅ **CORRECT**: `--draft` (sets True) or `--no-draft` (sets False)
|
|
54
57
|
- Updates existing feature metadata (title, outcomes, acceptance criteria, constraints, confidence, draft status)
|
|
55
58
|
- Works in CI/CD, Copilot, and interactive modes
|
|
56
59
|
- Example: `specfact plan update-feature --key FEATURE-001 --title "New Title" --outcomes "Outcome 1, Outcome 2"`
|
|
@@ -85,7 +85,7 @@ The `specfact plan update-feature` command:
|
|
|
85
85
|
- Acceptance criteria (optional, comma-separated)
|
|
86
86
|
- Constraints (optional, comma-separated)
|
|
87
87
|
- Confidence (optional, 0.0-1.0)
|
|
88
|
-
- Draft status (optional,
|
|
88
|
+
- Draft status (optional, boolean flag: `--draft` sets True, `--no-draft` sets False, omit to leave unchanged)
|
|
89
89
|
- Plan bundle path (optional, defaults to active plan or `.specfact/plans/main.bundle.yaml`)
|
|
90
90
|
|
|
91
91
|
**WAIT STATE**: If feature key is missing, ask the user:
|
|
@@ -155,10 +155,16 @@ specfact plan update-feature \
|
|
|
155
155
|
--constraints "Python 3.11+, Test coverage >= 80%" \
|
|
156
156
|
--plan <plan_path>
|
|
157
157
|
|
|
158
|
-
# Mark as draft
|
|
158
|
+
# Mark as draft (boolean flag: --draft sets True, --no-draft sets False)
|
|
159
159
|
specfact plan update-feature \
|
|
160
160
|
--key FEATURE-001 \
|
|
161
|
-
--draft
|
|
161
|
+
--draft \
|
|
162
|
+
--plan <plan_path>
|
|
163
|
+
|
|
164
|
+
# Unmark draft (set to False)
|
|
165
|
+
specfact plan update-feature \
|
|
166
|
+
--key FEATURE-001 \
|
|
167
|
+
--no-draft \
|
|
162
168
|
--plan <plan_path>
|
|
163
169
|
```
|
|
164
170
|
|
|
@@ -209,7 +215,9 @@ specfact plan update-feature \
|
|
|
209
215
|
- **Partial updates**: Only specified fields are updated, others remain unchanged
|
|
210
216
|
- **Comma-separated lists**: Outcomes, acceptance, and constraints use comma-separated strings
|
|
211
217
|
- **Confidence range**: Must be between 0.0 and 1.0
|
|
212
|
-
- **Draft status**:
|
|
218
|
+
- **Draft status**: Boolean flag - use `--draft` to set True, `--no-draft` to set False, omit to leave unchanged
|
|
219
|
+
- ❌ **WRONG**: `--draft true` or `--draft false` (Typer boolean flags don't accept values)
|
|
220
|
+
- ✅ **CORRECT**: `--draft` (sets True) or `--no-draft` (sets False) or omit (leaves unchanged)
|
|
213
221
|
|
|
214
222
|
### Field Guidelines
|
|
215
223
|
|
|
@@ -101,6 +101,7 @@ app = typer.Typer(
|
|
|
101
101
|
help="SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development",
|
|
102
102
|
add_completion=True, # Enable Typer's built-in completion (works natively for bash/zsh/fish without extensions)
|
|
103
103
|
rich_markup_mode="rich",
|
|
104
|
+
context_settings={"help_option_names": ["-h", "--help"]}, # Add -h as alias for --help
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
console = Console()
|
|
@@ -173,6 +174,12 @@ def main(
|
|
|
173
174
|
- Auto-detect from environment (CoPilot API, IDE integration)
|
|
174
175
|
- Default to CI/CD mode
|
|
175
176
|
"""
|
|
177
|
+
# Show help if no command provided (avoids user confusion)
|
|
178
|
+
if ctx.invoked_subcommand is None:
|
|
179
|
+
# Show help by calling Typer's help callback
|
|
180
|
+
ctx.get_help()
|
|
181
|
+
raise typer.Exit()
|
|
182
|
+
|
|
176
183
|
# Store mode in context for commands to access
|
|
177
184
|
if ctx.obj is None:
|
|
178
185
|
ctx.obj = {}
|
|
@@ -0,0 +1,294 @@
|
|
|
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
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from beartype import beartype
|
|
15
|
+
from icontract import ensure, require
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
|
|
19
|
+
from specfact_cli.telemetry import telemetry
|
|
20
|
+
from specfact_cli.utils.ide_setup import (
|
|
21
|
+
IDE_CONFIG,
|
|
22
|
+
copy_templates_to_ide,
|
|
23
|
+
detect_ide,
|
|
24
|
+
find_package_resources_path,
|
|
25
|
+
get_package_installation_locations,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(help="Initialize SpecFact for IDE integration")
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_valid_repo_path(path: Path) -> bool:
|
|
34
|
+
"""Check if path exists and is a directory."""
|
|
35
|
+
return path.exists() and path.is_dir()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.callback(invoke_without_command=True)
|
|
39
|
+
@require(lambda ide: ide in IDE_CONFIG or ide == "auto", "IDE must be valid or 'auto'")
|
|
40
|
+
@require(lambda repo: _is_valid_repo_path(repo), "Repo path must exist and be directory")
|
|
41
|
+
@ensure(lambda result: result is None, "Command should return None")
|
|
42
|
+
@beartype
|
|
43
|
+
def init(
|
|
44
|
+
ide: str = typer.Option(
|
|
45
|
+
"auto",
|
|
46
|
+
"--ide",
|
|
47
|
+
help="IDE type (auto, cursor, vscode, copilot, claude, gemini, qwen, opencode, windsurf, kilocode, auggie, roo, codebuddy, amp, q)",
|
|
48
|
+
),
|
|
49
|
+
repo: Path = typer.Option(
|
|
50
|
+
Path("."),
|
|
51
|
+
"--repo",
|
|
52
|
+
help="Repository path (default: current directory)",
|
|
53
|
+
exists=True,
|
|
54
|
+
file_okay=False,
|
|
55
|
+
dir_okay=True,
|
|
56
|
+
),
|
|
57
|
+
force: bool = typer.Option(
|
|
58
|
+
False,
|
|
59
|
+
"--force",
|
|
60
|
+
help="Overwrite existing files",
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Initialize SpecFact for IDE integration.
|
|
65
|
+
|
|
66
|
+
Copies prompt templates to IDE-specific locations so slash commands work.
|
|
67
|
+
This command detects the IDE type (or uses --ide flag) and copies
|
|
68
|
+
SpecFact prompt templates to the appropriate directory.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
specfact init # Auto-detect IDE
|
|
72
|
+
specfact init --ide cursor # Initialize for Cursor
|
|
73
|
+
specfact init --ide vscode --force # Overwrite existing files
|
|
74
|
+
specfact init --repo /path/to/repo --ide copilot
|
|
75
|
+
"""
|
|
76
|
+
telemetry_metadata = {
|
|
77
|
+
"ide": ide,
|
|
78
|
+
"force": force,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
with telemetry.track_command("init", telemetry_metadata) as record:
|
|
82
|
+
# Resolve repo path
|
|
83
|
+
repo_path = repo.resolve()
|
|
84
|
+
|
|
85
|
+
# Detect IDE
|
|
86
|
+
detected_ide = detect_ide(ide)
|
|
87
|
+
ide_config = IDE_CONFIG[detected_ide]
|
|
88
|
+
ide_name = ide_config["name"]
|
|
89
|
+
|
|
90
|
+
console.print()
|
|
91
|
+
console.print(Panel("[bold cyan]SpecFact IDE Setup[/bold cyan]", border_style="cyan"))
|
|
92
|
+
console.print(f"[cyan]Repository:[/cyan] {repo_path}")
|
|
93
|
+
console.print(f"[cyan]IDE:[/cyan] {ide_name} ({detected_ide})")
|
|
94
|
+
console.print()
|
|
95
|
+
|
|
96
|
+
# Find templates directory
|
|
97
|
+
# Priority order:
|
|
98
|
+
# 1. Development: relative to project root (resources/prompts)
|
|
99
|
+
# 2. Installed package: use importlib.resources to find package location
|
|
100
|
+
# 3. Fallback: try relative to this file (for edge cases)
|
|
101
|
+
templates_dir: Path | None = None
|
|
102
|
+
package_templates_dir: Path | None = None
|
|
103
|
+
tried_locations: list[Path] = []
|
|
104
|
+
|
|
105
|
+
# Try 1: Development mode - relative to repo root
|
|
106
|
+
dev_templates_dir = (repo_path / "resources" / "prompts").resolve()
|
|
107
|
+
tried_locations.append(dev_templates_dir)
|
|
108
|
+
console.print(f"[dim]Debug:[/dim] Trying development path: {dev_templates_dir}")
|
|
109
|
+
if dev_templates_dir.exists():
|
|
110
|
+
templates_dir = dev_templates_dir
|
|
111
|
+
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
112
|
+
else:
|
|
113
|
+
console.print("[dim]Debug:[/dim] Development path not found, trying installed package...")
|
|
114
|
+
# Try 2: Installed package - use importlib.resources
|
|
115
|
+
# Note: importlib is part of Python's standard library (since Python 3.1)
|
|
116
|
+
# importlib.resources.files() is available since Python 3.9
|
|
117
|
+
# Since we require Python >=3.11, this should always be available
|
|
118
|
+
# However, we catch exceptions for robustness (minimal installations, edge cases)
|
|
119
|
+
package_templates_dir = None
|
|
120
|
+
try:
|
|
121
|
+
import importlib.resources
|
|
122
|
+
|
|
123
|
+
console.print("[dim]Debug:[/dim] Using importlib.resources.files() API...")
|
|
124
|
+
# Use files() API (Python 3.9+) - recommended approach
|
|
125
|
+
resources_ref = importlib.resources.files("specfact_cli")
|
|
126
|
+
templates_ref = resources_ref / "resources" / "prompts"
|
|
127
|
+
# Convert Traversable to Path
|
|
128
|
+
# Traversable objects can be converted to Path via str()
|
|
129
|
+
# Use resolve() to handle Windows/Linux/macOS path differences
|
|
130
|
+
package_templates_dir = Path(str(templates_ref)).resolve()
|
|
131
|
+
tried_locations.append(package_templates_dir)
|
|
132
|
+
console.print(f"[dim]Debug:[/dim] Package templates path: {package_templates_dir}")
|
|
133
|
+
if package_templates_dir.exists():
|
|
134
|
+
templates_dir = package_templates_dir
|
|
135
|
+
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
136
|
+
else:
|
|
137
|
+
console.print("[yellow]⚠[/yellow] Package templates path exists but directory not found")
|
|
138
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
139
|
+
console.print(
|
|
140
|
+
f"[yellow]⚠[/yellow] importlib.resources not available or module not found: {type(e).__name__}: {e}"
|
|
141
|
+
)
|
|
142
|
+
console.print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
|
|
143
|
+
except (TypeError, AttributeError, ValueError) as e:
|
|
144
|
+
console.print(f"[yellow]⚠[/yellow] Error converting Traversable to Path: {e}")
|
|
145
|
+
console.print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
console.print(f"[yellow]⚠[/yellow] Unexpected error with importlib.resources: {type(e).__name__}: {e}")
|
|
148
|
+
console.print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
|
|
149
|
+
|
|
150
|
+
# Fallback: importlib.util.find_spec() + comprehensive package location search
|
|
151
|
+
if not templates_dir or not templates_dir.exists():
|
|
152
|
+
try:
|
|
153
|
+
import importlib.util
|
|
154
|
+
|
|
155
|
+
console.print("[dim]Debug:[/dim] Using importlib.util.find_spec() fallback...")
|
|
156
|
+
spec = importlib.util.find_spec("specfact_cli")
|
|
157
|
+
if spec and spec.origin:
|
|
158
|
+
# spec.origin points to __init__.py
|
|
159
|
+
# Go up to package root, then to resources/prompts
|
|
160
|
+
# Use resolve() for cross-platform compatibility
|
|
161
|
+
package_root = Path(spec.origin).parent.resolve()
|
|
162
|
+
package_templates_dir = (package_root / "resources" / "prompts").resolve()
|
|
163
|
+
tried_locations.append(package_templates_dir)
|
|
164
|
+
console.print(f"[dim]Debug:[/dim] Package root from spec.origin: {package_root}")
|
|
165
|
+
console.print(f"[dim]Debug:[/dim] Templates path from spec: {package_templates_dir}")
|
|
166
|
+
if package_templates_dir.exists():
|
|
167
|
+
templates_dir = package_templates_dir
|
|
168
|
+
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
169
|
+
else:
|
|
170
|
+
console.print("[yellow]⚠[/yellow] Templates path from spec not found")
|
|
171
|
+
else:
|
|
172
|
+
console.print("[yellow]⚠[/yellow] Could not find specfact_cli module spec")
|
|
173
|
+
if spec is None:
|
|
174
|
+
console.print("[dim]Debug:[/dim] spec is None")
|
|
175
|
+
elif not spec.origin:
|
|
176
|
+
console.print("[dim]Debug:[/dim] spec.origin is None or empty")
|
|
177
|
+
except Exception as e:
|
|
178
|
+
console.print(f"[yellow]⚠[/yellow] Error with importlib.util.find_spec(): {type(e).__name__}: {e}")
|
|
179
|
+
|
|
180
|
+
# Fallback: Comprehensive package location search (cross-platform)
|
|
181
|
+
if not templates_dir or not templates_dir.exists():
|
|
182
|
+
try:
|
|
183
|
+
console.print("[dim]Debug:[/dim] Searching all package installation locations...")
|
|
184
|
+
package_locations = get_package_installation_locations("specfact_cli")
|
|
185
|
+
console.print(f"[dim]Debug:[/dim] Found {len(package_locations)} possible package location(s)")
|
|
186
|
+
for i, loc in enumerate(package_locations, 1):
|
|
187
|
+
console.print(f"[dim]Debug:[/dim] {i}. {loc}")
|
|
188
|
+
# Check for resources/prompts in this package location
|
|
189
|
+
resource_path = (loc / "resources" / "prompts").resolve()
|
|
190
|
+
tried_locations.append(resource_path)
|
|
191
|
+
if resource_path.exists():
|
|
192
|
+
templates_dir = resource_path
|
|
193
|
+
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
194
|
+
break
|
|
195
|
+
if not templates_dir or not templates_dir.exists():
|
|
196
|
+
# Try using the helper function as a final attempt
|
|
197
|
+
console.print("[dim]Debug:[/dim] Trying find_package_resources_path() helper...")
|
|
198
|
+
resource_path = find_package_resources_path("specfact_cli", "resources/prompts")
|
|
199
|
+
if resource_path and resource_path.exists():
|
|
200
|
+
tried_locations.append(resource_path)
|
|
201
|
+
templates_dir = resource_path
|
|
202
|
+
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
203
|
+
else:
|
|
204
|
+
console.print("[yellow]⚠[/yellow] Resources not found in any package location")
|
|
205
|
+
except Exception as e:
|
|
206
|
+
console.print(f"[yellow]⚠[/yellow] Error searching package locations: {type(e).__name__}: {e}")
|
|
207
|
+
|
|
208
|
+
# Try 3: Fallback - relative to this file (for edge cases)
|
|
209
|
+
if not templates_dir or not templates_dir.exists():
|
|
210
|
+
try:
|
|
211
|
+
console.print("[dim]Debug:[/dim] Trying fallback: relative to __file__...")
|
|
212
|
+
# Get the directory containing this file (init.py)
|
|
213
|
+
# init.py is in: src/specfact_cli/commands/init.py
|
|
214
|
+
# Go up: commands -> specfact_cli -> src -> project root
|
|
215
|
+
current_file = Path(__file__).resolve()
|
|
216
|
+
fallback_dir = (current_file.parent.parent.parent.parent / "resources" / "prompts").resolve()
|
|
217
|
+
tried_locations.append(fallback_dir)
|
|
218
|
+
console.print(f"[dim]Debug:[/dim] Current file: {current_file}")
|
|
219
|
+
console.print(f"[dim]Debug:[/dim] Fallback templates path: {fallback_dir}")
|
|
220
|
+
if fallback_dir.exists():
|
|
221
|
+
templates_dir = fallback_dir
|
|
222
|
+
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
223
|
+
else:
|
|
224
|
+
console.print("[yellow]⚠[/yellow] Fallback path not found")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
console.print(f"[yellow]⚠[/yellow] Error with __file__ fallback: {type(e).__name__}: {e}")
|
|
227
|
+
|
|
228
|
+
if not templates_dir or not templates_dir.exists():
|
|
229
|
+
console.print()
|
|
230
|
+
console.print("[red]Error:[/red] Templates directory not found after all attempts")
|
|
231
|
+
console.print()
|
|
232
|
+
console.print("[yellow]Tried locations:[/yellow]")
|
|
233
|
+
for i, location in enumerate(tried_locations, 1):
|
|
234
|
+
exists = "✓" if location.exists() else "✗"
|
|
235
|
+
console.print(f" {i}. {exists} {location}")
|
|
236
|
+
console.print()
|
|
237
|
+
console.print("[yellow]Debug information:[/yellow]")
|
|
238
|
+
console.print(f" - Python version: {sys.version}")
|
|
239
|
+
console.print(f" - Platform: {sys.platform}")
|
|
240
|
+
console.print(f" - Current working directory: {Path.cwd()}")
|
|
241
|
+
console.print(f" - Repository path: {repo_path}")
|
|
242
|
+
console.print(f" - __file__ location: {Path(__file__).resolve()}")
|
|
243
|
+
try:
|
|
244
|
+
import importlib.util
|
|
245
|
+
|
|
246
|
+
spec = importlib.util.find_spec("specfact_cli")
|
|
247
|
+
if spec:
|
|
248
|
+
console.print(f" - Module spec found: {spec}")
|
|
249
|
+
console.print(f" - Module origin: {spec.origin}")
|
|
250
|
+
if spec.origin:
|
|
251
|
+
console.print(f" - Module location: {Path(spec.origin).parent.resolve()}")
|
|
252
|
+
else:
|
|
253
|
+
console.print(" - Module spec: Not found")
|
|
254
|
+
except Exception as e:
|
|
255
|
+
console.print(f" - Error checking module spec: {e}")
|
|
256
|
+
console.print()
|
|
257
|
+
console.print("[yellow]Expected location:[/yellow] resources/prompts/")
|
|
258
|
+
console.print("[yellow]Please ensure SpecFact is properly installed.[/yellow]")
|
|
259
|
+
raise typer.Exit(1)
|
|
260
|
+
|
|
261
|
+
console.print(f"[cyan]Templates:[/cyan] {templates_dir}")
|
|
262
|
+
console.print()
|
|
263
|
+
|
|
264
|
+
# Copy templates to IDE location
|
|
265
|
+
try:
|
|
266
|
+
copied_files, settings_path = copy_templates_to_ide(repo_path, detected_ide, templates_dir, force)
|
|
267
|
+
|
|
268
|
+
if not copied_files:
|
|
269
|
+
console.print(
|
|
270
|
+
"[yellow]No templates copied (all files already exist, use --force to overwrite)[/yellow]"
|
|
271
|
+
)
|
|
272
|
+
record({"files_copied": 0, "already_exists": True})
|
|
273
|
+
raise typer.Exit(0)
|
|
274
|
+
|
|
275
|
+
record(
|
|
276
|
+
{
|
|
277
|
+
"detected_ide": detected_ide,
|
|
278
|
+
"files_copied": len(copied_files),
|
|
279
|
+
"settings_updated": settings_path is not None,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
console.print()
|
|
284
|
+
console.print(Panel("[bold green]✓ Initialization Complete[/bold green]", border_style="green"))
|
|
285
|
+
console.print(f"[green]Copied {len(copied_files)} template(s) to {ide_config['folder']}[/green]")
|
|
286
|
+
if settings_path:
|
|
287
|
+
console.print(f"[green]Updated VS Code settings:[/green] {settings_path}")
|
|
288
|
+
console.print()
|
|
289
|
+
console.print("[dim]You can now use SpecFact slash commands in your IDE![/dim]")
|
|
290
|
+
console.print("[dim]Example: /specfact-import-from-code --repo . --confidence 0.7[/dim]")
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
console.print(f"[red]Error:[/red] Failed to initialize IDE integration: {e}")
|
|
294
|
+
raise typer.Exit(1) from e
|
|
@@ -774,7 +774,11 @@ def update_feature(
|
|
|
774
774
|
acceptance: str | None = typer.Option(None, "--acceptance", help="Acceptance criteria (comma-separated)"),
|
|
775
775
|
constraints: str | None = typer.Option(None, "--constraints", help="Constraints (comma-separated)"),
|
|
776
776
|
confidence: float | None = typer.Option(None, "--confidence", help="Confidence score (0.0-1.0)"),
|
|
777
|
-
draft: bool | None = typer.Option(
|
|
777
|
+
draft: bool | None = typer.Option(
|
|
778
|
+
None,
|
|
779
|
+
"--draft/--no-draft",
|
|
780
|
+
help="Mark as draft (use --draft to set True, --no-draft to set False, omit to leave unchanged)",
|
|
781
|
+
),
|
|
778
782
|
plan: Path | None = typer.Option(
|
|
779
783
|
None,
|
|
780
784
|
"--plan",
|
|
@@ -398,8 +398,8 @@ class SpecKitConverter:
|
|
|
398
398
|
feature_dir = self.repo_path / "specs" / f"{feature_num:03d}-{feature_name}"
|
|
399
399
|
feature_dir.mkdir(parents=True, exist_ok=True)
|
|
400
400
|
|
|
401
|
-
# Generate spec.md
|
|
402
|
-
spec_content = self._generate_spec_markdown(feature)
|
|
401
|
+
# Generate spec.md (pass calculated feature_num to avoid recalculation)
|
|
402
|
+
spec_content = self._generate_spec_markdown(feature, feature_num=feature_num)
|
|
403
403
|
(feature_dir / "spec.md").write_text(spec_content, encoding="utf-8")
|
|
404
404
|
|
|
405
405
|
# Generate plan.md
|
|
@@ -416,14 +416,29 @@ class SpecKitConverter:
|
|
|
416
416
|
|
|
417
417
|
@beartype
|
|
418
418
|
@require(lambda feature: isinstance(feature, Feature), "Must be Feature instance")
|
|
419
|
+
@require(
|
|
420
|
+
lambda feature_num: feature_num is None or feature_num > 0,
|
|
421
|
+
"Feature number must be None or positive",
|
|
422
|
+
)
|
|
419
423
|
@ensure(lambda result: isinstance(result, str), "Must return string")
|
|
420
424
|
@ensure(lambda result: len(result) > 0, "Result must be non-empty")
|
|
421
|
-
def _generate_spec_markdown(self, feature: Feature) -> str:
|
|
422
|
-
"""
|
|
425
|
+
def _generate_spec_markdown(self, feature: Feature, feature_num: int | None = None) -> str:
|
|
426
|
+
"""
|
|
427
|
+
Generate Spec-Kit spec.md content from SpecFact feature.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
feature: Feature to generate spec for
|
|
431
|
+
feature_num: Optional pre-calculated feature number (avoids recalculation with fallback)
|
|
432
|
+
"""
|
|
423
433
|
from datetime import datetime
|
|
424
434
|
|
|
425
435
|
# Extract feature branch from feature key (FEATURE-001 -> 001-feature-name)
|
|
426
|
-
feature_num
|
|
436
|
+
# Use provided feature_num if available, otherwise extract from key (with fallback to 1)
|
|
437
|
+
if feature_num is None:
|
|
438
|
+
feature_num = self._extract_feature_number(feature.key)
|
|
439
|
+
if feature_num == 0:
|
|
440
|
+
# Fallback: use 1 if no number found (shouldn't happen if called from convert_to_speckit)
|
|
441
|
+
feature_num = 1
|
|
427
442
|
feature_name = self._to_feature_dir_name(feature.title)
|
|
428
443
|
feature_branch = f"{feature_num:03d}-{feature_name}"
|
|
429
444
|
|
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
|
+
import site
|
|
13
|
+
import sys
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Literal
|
|
14
16
|
|
|
@@ -387,3 +389,164 @@ def create_vscode_settings(repo_path: Path, settings_file: str) -> Path | None:
|
|
|
387
389
|
|
|
388
390
|
console.print(f"[green]Updated:[/green] {settings_path}")
|
|
389
391
|
return settings_path
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@beartype
|
|
395
|
+
@ensure(
|
|
396
|
+
lambda result: isinstance(result, list) and all(isinstance(p, Path) for p in result), "Must return list of Paths"
|
|
397
|
+
)
|
|
398
|
+
def get_package_installation_locations(package_name: str) -> list[Path]:
|
|
399
|
+
"""
|
|
400
|
+
Get all possible installation locations for a Python package across different OS and installation types.
|
|
401
|
+
|
|
402
|
+
This function searches for package locations in:
|
|
403
|
+
- User site-packages (per-user installations: ~/.local/lib/python3.X/site-packages)
|
|
404
|
+
- System site-packages (global installations: /usr/lib/python3.X/site-packages, C:\\Python3X\\Lib\\site-packages)
|
|
405
|
+
- Virtual environments (venv, conda, etc.)
|
|
406
|
+
- uvx cache locations (~/.cache/uv/archive-v0/...)
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
package_name: Name of the package to locate (e.g., "specfact_cli")
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
List of Path objects representing possible package installation locations
|
|
413
|
+
|
|
414
|
+
Examples:
|
|
415
|
+
>>> locations = get_package_installation_locations("specfact_cli")
|
|
416
|
+
>>> len(locations) > 0
|
|
417
|
+
True
|
|
418
|
+
"""
|
|
419
|
+
locations: list[Path] = []
|
|
420
|
+
|
|
421
|
+
# Method 1: Use importlib.util.find_spec() to find the actual installed location
|
|
422
|
+
try:
|
|
423
|
+
import importlib.util
|
|
424
|
+
|
|
425
|
+
spec = importlib.util.find_spec(package_name)
|
|
426
|
+
if spec and spec.origin:
|
|
427
|
+
package_path = Path(spec.origin).parent.resolve()
|
|
428
|
+
locations.append(package_path)
|
|
429
|
+
except Exception:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# Method 2: Check all site-packages directories (user + system)
|
|
433
|
+
try:
|
|
434
|
+
# User site-packages (per-user installation)
|
|
435
|
+
# Linux/macOS: ~/.local/lib/python3.X/site-packages
|
|
436
|
+
# Windows: %APPDATA%\\Python\\Python3X\\site-packages
|
|
437
|
+
user_site = site.getusersitepackages()
|
|
438
|
+
if user_site:
|
|
439
|
+
user_package_path = Path(user_site) / package_name
|
|
440
|
+
if user_package_path.exists():
|
|
441
|
+
locations.append(user_package_path.resolve())
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
# System site-packages (global installation)
|
|
447
|
+
# Linux: /usr/lib/python3.X/dist-packages, /usr/local/lib/python3.X/dist-packages
|
|
448
|
+
# macOS: /Library/Frameworks/Python.framework/Versions/X/lib/pythonX.X/site-packages
|
|
449
|
+
# Windows: C:\\Python3X\\Lib\\site-packages
|
|
450
|
+
system_sites = site.getsitepackages()
|
|
451
|
+
for site_path in system_sites:
|
|
452
|
+
system_package_path = Path(site_path) / package_name
|
|
453
|
+
if system_package_path.exists():
|
|
454
|
+
locations.append(system_package_path.resolve())
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
# Method 3: Check sys.path for additional locations (virtual environments, etc.)
|
|
459
|
+
for path_str in sys.path:
|
|
460
|
+
if not path_str or path_str == "":
|
|
461
|
+
continue
|
|
462
|
+
try:
|
|
463
|
+
path = Path(path_str).resolve()
|
|
464
|
+
if path.exists() and path.is_dir():
|
|
465
|
+
# Check if package is directly in this path
|
|
466
|
+
package_path = path / package_name
|
|
467
|
+
if package_path.exists():
|
|
468
|
+
locations.append(package_path.resolve())
|
|
469
|
+
# Check if this is a site-packages directory
|
|
470
|
+
if path.name == "site-packages" or "site-packages" in path.parts:
|
|
471
|
+
package_path = path / package_name
|
|
472
|
+
if package_path.exists():
|
|
473
|
+
locations.append(package_path.resolve())
|
|
474
|
+
except Exception:
|
|
475
|
+
continue
|
|
476
|
+
|
|
477
|
+
# Method 4: Check uvx cache locations (common on Linux/macOS/Windows)
|
|
478
|
+
# uvx stores packages in cache directories with varying structures
|
|
479
|
+
if sys.platform != "win32":
|
|
480
|
+
# Linux/macOS: ~/.cache/uv/archive-v0/.../lib/python3.X/site-packages/
|
|
481
|
+
uvx_cache_base = Path.home() / ".cache" / "uv" / "archive-v0"
|
|
482
|
+
if uvx_cache_base.exists():
|
|
483
|
+
for archive_dir in uvx_cache_base.iterdir():
|
|
484
|
+
if archive_dir.is_dir():
|
|
485
|
+
# Look for site-packages directories (rglob finds all matches)
|
|
486
|
+
for site_packages_dir in archive_dir.rglob("site-packages"):
|
|
487
|
+
if site_packages_dir.is_dir():
|
|
488
|
+
package_path = site_packages_dir / package_name
|
|
489
|
+
if package_path.exists():
|
|
490
|
+
locations.append(package_path.resolve())
|
|
491
|
+
else:
|
|
492
|
+
# Windows: Check %LOCALAPPDATA%\\uv\\cache\\archive-v0\\
|
|
493
|
+
localappdata = os.environ.get("LOCALAPPDATA")
|
|
494
|
+
if localappdata:
|
|
495
|
+
uvx_cache_base = Path(localappdata) / "uv" / "cache" / "archive-v0"
|
|
496
|
+
if uvx_cache_base.exists():
|
|
497
|
+
for archive_dir in uvx_cache_base.iterdir():
|
|
498
|
+
if archive_dir.is_dir():
|
|
499
|
+
# Look for site-packages directories
|
|
500
|
+
for site_packages_dir in archive_dir.rglob("site-packages"):
|
|
501
|
+
if site_packages_dir.is_dir():
|
|
502
|
+
package_path = site_packages_dir / package_name
|
|
503
|
+
if package_path.exists():
|
|
504
|
+
locations.append(package_path.resolve())
|
|
505
|
+
|
|
506
|
+
# Remove duplicates while preserving order
|
|
507
|
+
seen = set()
|
|
508
|
+
unique_locations: list[Path] = []
|
|
509
|
+
for loc in locations:
|
|
510
|
+
loc_str = str(loc)
|
|
511
|
+
if loc_str not in seen:
|
|
512
|
+
seen.add(loc_str)
|
|
513
|
+
unique_locations.append(loc)
|
|
514
|
+
|
|
515
|
+
return unique_locations
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@beartype
|
|
519
|
+
@require(lambda package_name: isinstance(package_name, str) and len(package_name) > 0, "Package name must be non-empty")
|
|
520
|
+
@ensure(
|
|
521
|
+
lambda result: result is None or (isinstance(result, Path) and result.exists()),
|
|
522
|
+
"Result must be None or existing Path",
|
|
523
|
+
)
|
|
524
|
+
def find_package_resources_path(package_name: str, resource_subpath: str) -> Path | None:
|
|
525
|
+
"""
|
|
526
|
+
Find the path to a resource within an installed package.
|
|
527
|
+
|
|
528
|
+
Searches across all possible installation locations (user, system, venv, uvx cache)
|
|
529
|
+
to find the package and then locates the resource subpath.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
package_name: Name of the package (e.g., "specfact_cli")
|
|
533
|
+
resource_subpath: Subpath within the package (e.g., "resources/prompts")
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Path to the resource directory if found, None otherwise
|
|
537
|
+
|
|
538
|
+
Examples:
|
|
539
|
+
>>> path = find_package_resources_path("specfact_cli", "resources/prompts")
|
|
540
|
+
>>> path is None or path.exists()
|
|
541
|
+
True
|
|
542
|
+
"""
|
|
543
|
+
# Get all possible package installation locations
|
|
544
|
+
package_locations = get_package_installation_locations(package_name)
|
|
545
|
+
|
|
546
|
+
# Try each location
|
|
547
|
+
for package_path in package_locations:
|
|
548
|
+
resource_path = (package_path / resource_subpath).resolve()
|
|
549
|
+
if resource_path.exists():
|
|
550
|
+
return resource_path
|
|
551
|
+
|
|
552
|
+
return None
|
|
@@ -1,143 +0,0 @@
|
|
|
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.telemetry import telemetry
|
|
19
|
-
from specfact_cli.utils.ide_setup import IDE_CONFIG, copy_templates_to_ide, detect_ide
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
app = typer.Typer(help="Initialize SpecFact for IDE integration")
|
|
23
|
-
console = Console()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _is_valid_repo_path(path: Path) -> bool:
|
|
27
|
-
"""Check if path exists and is a directory."""
|
|
28
|
-
return path.exists() and path.is_dir()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@app.callback(invoke_without_command=True)
|
|
32
|
-
@require(lambda ide: ide in IDE_CONFIG or ide == "auto", "IDE must be valid or 'auto'")
|
|
33
|
-
@require(lambda repo: _is_valid_repo_path(repo), "Repo path must exist and be directory")
|
|
34
|
-
@ensure(lambda result: result is None, "Command should return None")
|
|
35
|
-
@beartype
|
|
36
|
-
def init(
|
|
37
|
-
ide: str = typer.Option(
|
|
38
|
-
"auto",
|
|
39
|
-
"--ide",
|
|
40
|
-
help="IDE type (auto, cursor, vscode, copilot, claude, gemini, qwen, opencode, windsurf, kilocode, auggie, roo, codebuddy, amp, q)",
|
|
41
|
-
),
|
|
42
|
-
repo: Path = typer.Option(
|
|
43
|
-
Path("."),
|
|
44
|
-
"--repo",
|
|
45
|
-
help="Repository path (default: current directory)",
|
|
46
|
-
exists=True,
|
|
47
|
-
file_okay=False,
|
|
48
|
-
dir_okay=True,
|
|
49
|
-
),
|
|
50
|
-
force: bool = typer.Option(
|
|
51
|
-
False,
|
|
52
|
-
"--force",
|
|
53
|
-
help="Overwrite existing files",
|
|
54
|
-
),
|
|
55
|
-
) -> None:
|
|
56
|
-
"""
|
|
57
|
-
Initialize SpecFact for IDE integration.
|
|
58
|
-
|
|
59
|
-
Copies prompt templates to IDE-specific locations so slash commands work.
|
|
60
|
-
This command detects the IDE type (or uses --ide flag) and copies
|
|
61
|
-
SpecFact prompt templates to the appropriate directory.
|
|
62
|
-
|
|
63
|
-
Examples:
|
|
64
|
-
specfact init # Auto-detect IDE
|
|
65
|
-
specfact init --ide cursor # Initialize for Cursor
|
|
66
|
-
specfact init --ide vscode --force # Overwrite existing files
|
|
67
|
-
specfact init --repo /path/to/repo --ide copilot
|
|
68
|
-
"""
|
|
69
|
-
telemetry_metadata = {
|
|
70
|
-
"ide": ide,
|
|
71
|
-
"force": force,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
with telemetry.track_command("init", telemetry_metadata) as record:
|
|
75
|
-
# Resolve repo path
|
|
76
|
-
repo_path = repo.resolve()
|
|
77
|
-
|
|
78
|
-
# Detect IDE
|
|
79
|
-
detected_ide = detect_ide(ide)
|
|
80
|
-
ide_config = IDE_CONFIG[detected_ide]
|
|
81
|
-
ide_name = ide_config["name"]
|
|
82
|
-
|
|
83
|
-
console.print()
|
|
84
|
-
console.print(Panel("[bold cyan]SpecFact IDE Setup[/bold cyan]", border_style="cyan"))
|
|
85
|
-
console.print(f"[cyan]Repository:[/cyan] {repo_path}")
|
|
86
|
-
console.print(f"[cyan]IDE:[/cyan] {ide_name} ({detected_ide})")
|
|
87
|
-
console.print()
|
|
88
|
-
|
|
89
|
-
# Find templates directory
|
|
90
|
-
# Try relative to project root first (for development)
|
|
91
|
-
templates_dir = repo_path / "resources" / "prompts"
|
|
92
|
-
if not templates_dir.exists():
|
|
93
|
-
# Try relative to installed package (for distribution)
|
|
94
|
-
import importlib.util
|
|
95
|
-
|
|
96
|
-
spec = importlib.util.find_spec("specfact_cli")
|
|
97
|
-
if spec and spec.origin:
|
|
98
|
-
package_dir = Path(spec.origin).parent.parent
|
|
99
|
-
templates_dir = package_dir / "site-packages" / "specfact_cli" / "resources" / "prompts"
|
|
100
|
-
if not templates_dir.exists():
|
|
101
|
-
# Fallback: try resources/prompts in project root
|
|
102
|
-
templates_dir = Path(__file__).parent.parent.parent.parent / "resources" / "prompts"
|
|
103
|
-
|
|
104
|
-
if not templates_dir.exists():
|
|
105
|
-
console.print(f"[red]Error:[/red] Templates directory not found: {templates_dir}")
|
|
106
|
-
console.print("[yellow]Expected location:[/yellow] resources/prompts/")
|
|
107
|
-
console.print("[yellow]Please ensure SpecFact is properly installed.[/yellow]")
|
|
108
|
-
raise typer.Exit(1)
|
|
109
|
-
|
|
110
|
-
console.print(f"[cyan]Templates:[/cyan] {templates_dir}")
|
|
111
|
-
console.print()
|
|
112
|
-
|
|
113
|
-
# Copy templates to IDE location
|
|
114
|
-
try:
|
|
115
|
-
copied_files, settings_path = copy_templates_to_ide(repo_path, detected_ide, templates_dir, force)
|
|
116
|
-
|
|
117
|
-
if not copied_files:
|
|
118
|
-
console.print(
|
|
119
|
-
"[yellow]No templates copied (all files already exist, use --force to overwrite)[/yellow]"
|
|
120
|
-
)
|
|
121
|
-
record({"files_copied": 0, "already_exists": True})
|
|
122
|
-
raise typer.Exit(0)
|
|
123
|
-
|
|
124
|
-
record(
|
|
125
|
-
{
|
|
126
|
-
"detected_ide": detected_ide,
|
|
127
|
-
"files_copied": len(copied_files),
|
|
128
|
-
"settings_updated": settings_path is not None,
|
|
129
|
-
}
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
console.print()
|
|
133
|
-
console.print(Panel("[bold green]✓ Initialization Complete[/bold green]", border_style="green"))
|
|
134
|
-
console.print(f"[green]Copied {len(copied_files)} template(s) to {ide_config['folder']}[/green]")
|
|
135
|
-
if settings_path:
|
|
136
|
-
console.print(f"[green]Updated VS Code settings:[/green] {settings_path}")
|
|
137
|
-
console.print()
|
|
138
|
-
console.print("[dim]You can now use SpecFact slash commands in your IDE![/dim]")
|
|
139
|
-
console.print("[dim]Example: /specfact-import-from-code --repo . --confidence 0.7[/dim]")
|
|
140
|
-
|
|
141
|
-
except Exception as e:
|
|
142
|
-
console.print(f"[red]Error:[/red] Failed to initialize IDE integration: {e}")
|
|
143
|
-
raise typer.Exit(1) from e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/control_flow_analyzer.py
RENAMED
|
File without changes
|
{specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/requirement_extractor.py
RENAMED
|
File without changes
|
{specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/analyzers/test_pattern_extractor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfact_cli-0.6.4 → specfact_cli-0.6.6}/src/specfact_cli/enrichers/constitution_enricher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|