forge-dev 0.1.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.
- forge_core/__init__.py +3 -0
- forge_core/agents/__init__.py +1 -0
- forge_core/auditor.py +330 -0
- forge_core/cli.py +552 -0
- forge_core/detector.py +209 -0
- forge_core/editor_bridge.py +543 -0
- forge_core/models.py +332 -0
- forge_core/phases/__init__.py +1 -0
- forge_core/phases/coherence.py +293 -0
- forge_core/phases/context.py +264 -0
- forge_core/phases/intake.py +340 -0
- forge_core/registry.py +247 -0
- forge_core/standards/api-first-design.yaml +24 -0
- forge_core/standards/microservice-packaging.yaml +30 -0
- forge_core/standards/observability.yaml +31 -0
- forge_core/standards/security-baseline.yaml +43 -0
- forge_core/standards/type-safety.yaml +23 -0
- forge_core/templates/__init__.py +1 -0
- forge_core/utils/__init__.py +1 -0
- forge_dev-0.1.0.dist-info/METADATA +134 -0
- forge_dev-0.1.0.dist-info/RECORD +25 -0
- forge_dev-0.1.0.dist-info/WHEEL +4 -0
- forge_dev-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_server/__init__.py +1 -0
- mcp_server/server.py +1086 -0
forge_core/detector.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Project Detector — understands the state of the current directory.
|
|
2
|
+
|
|
3
|
+
Detects whether we're in:
|
|
4
|
+
- An empty directory (greenfield)
|
|
5
|
+
- A directory with docs but no code
|
|
6
|
+
- A directory with existing code
|
|
7
|
+
- A directory already initialized with Forge
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from forge_core.models import ProjectState
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# File patterns that indicate different project states
|
|
18
|
+
CODE_EXTENSIONS = {
|
|
19
|
+
".py", ".js", ".ts", ".jsx", ".tsx", ".cs", ".vb", ".java",
|
|
20
|
+
".go", ".rs", ".rb", ".php", ".swift", ".kt",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
DOC_EXTENSIONS = {
|
|
24
|
+
".md", ".txt", ".pdf", ".docx", ".doc", ".rtf",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
CONFIG_FILES = {
|
|
28
|
+
"package.json", "pyproject.toml", "requirements.txt", "Cargo.toml",
|
|
29
|
+
"go.mod", "pom.xml", "build.gradle", "Gemfile", "composer.json",
|
|
30
|
+
"Makefile", "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
IGNORE_DIRS = {
|
|
34
|
+
".git", ".forge", "node_modules", "__pycache__", ".venv", "venv",
|
|
35
|
+
".tox", ".mypy_cache", ".pytest_cache", "dist", "build", ".next",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProjectDetection:
|
|
40
|
+
"""Result of detecting project state."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, path: Path) -> None:
|
|
43
|
+
self.path = path.resolve()
|
|
44
|
+
self.state: ProjectState = ProjectState.EMPTY
|
|
45
|
+
self.has_forge: bool = False
|
|
46
|
+
self.has_git: bool = False
|
|
47
|
+
self.code_files: list[Path] = []
|
|
48
|
+
self.doc_files: list[Path] = []
|
|
49
|
+
self.config_files: list[Path] = []
|
|
50
|
+
self.detected_stack: dict[str, str] = {}
|
|
51
|
+
self.forge_context_path: Path | None = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_empty(self) -> bool:
|
|
55
|
+
return self.state == ProjectState.EMPTY
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_greenfield(self) -> bool:
|
|
59
|
+
return self.state in (ProjectState.EMPTY, ProjectState.HAS_DOCS)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def has_existing_code(self) -> bool:
|
|
63
|
+
return self.state == ProjectState.HAS_CODE
|
|
64
|
+
|
|
65
|
+
def summary(self) -> str:
|
|
66
|
+
"""Human-readable summary of detection."""
|
|
67
|
+
lines = [f"Project path: {self.path}"]
|
|
68
|
+
lines.append(f"State: {self.state.value}")
|
|
69
|
+
|
|
70
|
+
if self.has_forge:
|
|
71
|
+
lines.append("Forge: Already initialized")
|
|
72
|
+
if self.has_git:
|
|
73
|
+
lines.append("Git: Yes")
|
|
74
|
+
if self.code_files:
|
|
75
|
+
lines.append(f"Code files: {len(self.code_files)}")
|
|
76
|
+
if self.doc_files:
|
|
77
|
+
lines.append(f"Documents: {len(self.doc_files)}")
|
|
78
|
+
if self.config_files:
|
|
79
|
+
lines.append(f"Config files: {[f.name for f in self.config_files]}")
|
|
80
|
+
if self.detected_stack:
|
|
81
|
+
lines.append(f"Detected stack: {self.detected_stack}")
|
|
82
|
+
|
|
83
|
+
return "\n".join(lines)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def detect_project(path: Path | None = None) -> ProjectDetection:
|
|
87
|
+
"""Detect the state of a project directory.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path: Directory to analyze. Defaults to current directory.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
ProjectDetection with all findings.
|
|
94
|
+
"""
|
|
95
|
+
if path is None:
|
|
96
|
+
path = Path.cwd()
|
|
97
|
+
path = path.resolve()
|
|
98
|
+
|
|
99
|
+
detection = ProjectDetection(path)
|
|
100
|
+
|
|
101
|
+
# Check for .forge/
|
|
102
|
+
forge_dir = path / ".forge"
|
|
103
|
+
if forge_dir.exists() and (forge_dir / "context.yaml").exists():
|
|
104
|
+
detection.has_forge = True
|
|
105
|
+
detection.forge_context_path = forge_dir / "context.yaml"
|
|
106
|
+
detection.state = ProjectState.HAS_FORGE
|
|
107
|
+
|
|
108
|
+
# Check for .git/
|
|
109
|
+
detection.has_git = (path / ".git").exists()
|
|
110
|
+
|
|
111
|
+
# Scan files (max 2 levels deep for speed)
|
|
112
|
+
_scan_directory(path, detection, max_depth=3, current_depth=0)
|
|
113
|
+
|
|
114
|
+
# Determine state if not already set by .forge
|
|
115
|
+
if not detection.has_forge:
|
|
116
|
+
if detection.code_files or detection.config_files:
|
|
117
|
+
detection.state = ProjectState.HAS_CODE
|
|
118
|
+
elif detection.doc_files:
|
|
119
|
+
detection.state = ProjectState.HAS_DOCS
|
|
120
|
+
else:
|
|
121
|
+
detection.state = ProjectState.EMPTY
|
|
122
|
+
|
|
123
|
+
# Detect stack from config files
|
|
124
|
+
detection.detected_stack = _detect_stack(path, detection.config_files)
|
|
125
|
+
|
|
126
|
+
return detection
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _scan_directory(
|
|
130
|
+
path: Path, detection: ProjectDetection, max_depth: int, current_depth: int
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Recursively scan directory for relevant files."""
|
|
133
|
+
if current_depth >= max_depth:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
entries = sorted(path.iterdir())
|
|
138
|
+
except PermissionError:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
for entry in entries:
|
|
142
|
+
if entry.name.startswith(".") and entry.name not in (".env.example",):
|
|
143
|
+
continue
|
|
144
|
+
if entry.name in IGNORE_DIRS:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if entry.is_file():
|
|
148
|
+
suffix = entry.suffix.lower()
|
|
149
|
+
if suffix in CODE_EXTENSIONS:
|
|
150
|
+
detection.code_files.append(entry)
|
|
151
|
+
elif suffix in DOC_EXTENSIONS:
|
|
152
|
+
detection.doc_files.append(entry)
|
|
153
|
+
if entry.name in CONFIG_FILES:
|
|
154
|
+
detection.config_files.append(entry)
|
|
155
|
+
elif entry.is_dir():
|
|
156
|
+
_scan_directory(entry, detection, max_depth, current_depth + 1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _detect_stack(path: Path, config_files: list[Path]) -> dict[str, str]:
|
|
160
|
+
"""Detect the technology stack from config files."""
|
|
161
|
+
stack: dict[str, str] = {}
|
|
162
|
+
config_names = {f.name for f in config_files}
|
|
163
|
+
|
|
164
|
+
# Backend detection
|
|
165
|
+
if "pyproject.toml" in config_names or "requirements.txt" in config_names:
|
|
166
|
+
stack["language"] = "python"
|
|
167
|
+
# Try to detect framework
|
|
168
|
+
for cfg in config_files:
|
|
169
|
+
if cfg.name in ("pyproject.toml", "requirements.txt"):
|
|
170
|
+
try:
|
|
171
|
+
content = cfg.read_text(errors="ignore").lower()
|
|
172
|
+
if "fastapi" in content:
|
|
173
|
+
stack["backend"] = "python/fastapi"
|
|
174
|
+
elif "django" in content:
|
|
175
|
+
stack["backend"] = "python/django"
|
|
176
|
+
elif "flask" in content:
|
|
177
|
+
stack["backend"] = "python/flask"
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
if "package.json" in config_names:
|
|
182
|
+
stack.setdefault("language", "node")
|
|
183
|
+
for cfg in config_files:
|
|
184
|
+
if cfg.name == "package.json":
|
|
185
|
+
try:
|
|
186
|
+
content = cfg.read_text(errors="ignore").lower()
|
|
187
|
+
if "express" in content:
|
|
188
|
+
stack["backend"] = "node/express"
|
|
189
|
+
elif "fastify" in content:
|
|
190
|
+
stack["backend"] = "node/fastify"
|
|
191
|
+
elif "nestjs" in content or "@nestjs" in content:
|
|
192
|
+
stack["backend"] = "node/nestjs"
|
|
193
|
+
# Frontend detection
|
|
194
|
+
if '"react"' in content or '"react-dom"' in content:
|
|
195
|
+
stack["frontend"] = "react"
|
|
196
|
+
elif '"next"' in content:
|
|
197
|
+
stack["frontend"] = "nextjs"
|
|
198
|
+
elif '"vue"' in content:
|
|
199
|
+
stack["frontend"] = "vue"
|
|
200
|
+
elif '"svelte"' in content:
|
|
201
|
+
stack["frontend"] = "svelte"
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# Docker detection
|
|
206
|
+
if "Dockerfile" in config_names or "docker-compose.yml" in config_names:
|
|
207
|
+
stack["containerized"] = "true"
|
|
208
|
+
|
|
209
|
+
return stack
|