autoforge-ai 0.1.0
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.
- package/.claude/commands/check-code.md +32 -0
- package/.claude/commands/checkpoint.md +40 -0
- package/.claude/commands/create-spec.md +613 -0
- package/.claude/commands/expand-project.md +234 -0
- package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
- package/.claude/commands/review-pr.md +75 -0
- package/.claude/templates/app_spec.template.txt +331 -0
- package/.claude/templates/coding_prompt.template.md +265 -0
- package/.claude/templates/initializer_prompt.template.md +354 -0
- package/.claude/templates/testing_prompt.template.md +146 -0
- package/.env.example +64 -0
- package/LICENSE.md +676 -0
- package/README.md +423 -0
- package/agent.py +444 -0
- package/api/__init__.py +10 -0
- package/api/database.py +536 -0
- package/api/dependency_resolver.py +449 -0
- package/api/migration.py +156 -0
- package/auth.py +83 -0
- package/autoforge_paths.py +315 -0
- package/autonomous_agent_demo.py +293 -0
- package/bin/autoforge.js +3 -0
- package/client.py +607 -0
- package/env_constants.py +27 -0
- package/examples/OPTIMIZE_CONFIG.md +230 -0
- package/examples/README.md +531 -0
- package/examples/org_config.yaml +172 -0
- package/examples/project_allowed_commands.yaml +139 -0
- package/lib/cli.js +791 -0
- package/mcp_server/__init__.py +1 -0
- package/mcp_server/feature_mcp.py +988 -0
- package/package.json +53 -0
- package/parallel_orchestrator.py +1800 -0
- package/progress.py +247 -0
- package/prompts.py +427 -0
- package/pyproject.toml +17 -0
- package/rate_limit_utils.py +132 -0
- package/registry.py +614 -0
- package/requirements-prod.txt +14 -0
- package/security.py +959 -0
- package/server/__init__.py +17 -0
- package/server/main.py +261 -0
- package/server/routers/__init__.py +32 -0
- package/server/routers/agent.py +177 -0
- package/server/routers/assistant_chat.py +327 -0
- package/server/routers/devserver.py +309 -0
- package/server/routers/expand_project.py +239 -0
- package/server/routers/features.py +746 -0
- package/server/routers/filesystem.py +514 -0
- package/server/routers/projects.py +524 -0
- package/server/routers/schedules.py +356 -0
- package/server/routers/settings.py +127 -0
- package/server/routers/spec_creation.py +357 -0
- package/server/routers/terminal.py +453 -0
- package/server/schemas.py +593 -0
- package/server/services/__init__.py +36 -0
- package/server/services/assistant_chat_session.py +496 -0
- package/server/services/assistant_database.py +304 -0
- package/server/services/chat_constants.py +57 -0
- package/server/services/dev_server_manager.py +557 -0
- package/server/services/expand_chat_session.py +399 -0
- package/server/services/process_manager.py +657 -0
- package/server/services/project_config.py +475 -0
- package/server/services/scheduler_service.py +683 -0
- package/server/services/spec_chat_session.py +502 -0
- package/server/services/terminal_manager.py +756 -0
- package/server/utils/__init__.py +1 -0
- package/server/utils/process_utils.py +134 -0
- package/server/utils/project_helpers.py +32 -0
- package/server/utils/validation.py +54 -0
- package/server/websocket.py +903 -0
- package/start.py +456 -0
- package/ui/dist/assets/index-8W_wmZzz.js +168 -0
- package/ui/dist/assets/index-B47Ubhox.css +1 -0
- package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
- package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
- package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
- package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
- package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
- package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/ollama.png +0 -0
- package/ui/dist/vite.svg +6 -0
- package/ui/package.json +57 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Configuration Service
|
|
3
|
+
=============================
|
|
4
|
+
|
|
5
|
+
Handles project type detection and dev command configuration.
|
|
6
|
+
Detects project types by scanning for configuration files and provides
|
|
7
|
+
default or custom dev commands for each project.
|
|
8
|
+
|
|
9
|
+
Configuration is stored in {project_dir}/.autoforge/config.json.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TypedDict
|
|
16
|
+
|
|
17
|
+
# Python 3.11+ has tomllib in the standard library
|
|
18
|
+
try:
|
|
19
|
+
import tomllib
|
|
20
|
+
except ImportError:
|
|
21
|
+
tomllib = None # type: ignore[assignment]
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Path Validation
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_project_dir(project_dir: Path) -> Path:
|
|
32
|
+
"""
|
|
33
|
+
Validate and resolve the project directory.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
project_dir: Path to the project directory.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Resolved Path object.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If project_dir is not a valid directory.
|
|
43
|
+
"""
|
|
44
|
+
resolved = Path(project_dir).resolve()
|
|
45
|
+
|
|
46
|
+
if not resolved.exists():
|
|
47
|
+
raise ValueError(f"Project directory does not exist: {resolved}")
|
|
48
|
+
if not resolved.is_dir():
|
|
49
|
+
raise ValueError(f"Path is not a directory: {resolved}")
|
|
50
|
+
|
|
51
|
+
return resolved
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# Type Definitions
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ProjectConfig(TypedDict):
|
|
59
|
+
"""Full project configuration response."""
|
|
60
|
+
detected_type: str | None
|
|
61
|
+
detected_command: str | None
|
|
62
|
+
custom_command: str | None
|
|
63
|
+
effective_command: str | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Project Type Definitions
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
# Mapping of project types to their default dev commands
|
|
71
|
+
PROJECT_TYPE_COMMANDS: dict[str, str] = {
|
|
72
|
+
"nodejs-vite": "npm run dev",
|
|
73
|
+
"nodejs-cra": "npm start",
|
|
74
|
+
"python-poetry": "poetry run python -m uvicorn main:app --reload",
|
|
75
|
+
"python-django": "python manage.py runserver",
|
|
76
|
+
"python-fastapi": "python -m uvicorn main:app --reload",
|
|
77
|
+
"rust": "cargo run",
|
|
78
|
+
"go": "go run .",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# =============================================================================
|
|
83
|
+
# Configuration File Handling
|
|
84
|
+
# =============================================================================
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_config_path(project_dir: Path) -> Path:
|
|
88
|
+
"""
|
|
89
|
+
Get the path to the project config file.
|
|
90
|
+
|
|
91
|
+
Checks the new .autoforge/ location first, falls back to .autocoder/
|
|
92
|
+
for backward compatibility.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
project_dir: Path to the project directory.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Path to the config.json file in the appropriate directory.
|
|
99
|
+
"""
|
|
100
|
+
new_path = project_dir / ".autoforge" / "config.json"
|
|
101
|
+
if new_path.exists():
|
|
102
|
+
return new_path
|
|
103
|
+
old_path = project_dir / ".autocoder" / "config.json"
|
|
104
|
+
if old_path.exists():
|
|
105
|
+
return old_path
|
|
106
|
+
return new_path
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_config(project_dir: Path) -> dict:
|
|
110
|
+
"""
|
|
111
|
+
Load the project configuration from disk.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
project_dir: Path to the project directory.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Configuration dictionary, or empty dict if file doesn't exist or is invalid.
|
|
118
|
+
"""
|
|
119
|
+
config_path = _get_config_path(project_dir)
|
|
120
|
+
|
|
121
|
+
if not config_path.exists():
|
|
122
|
+
return {}
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
126
|
+
config = json.load(f)
|
|
127
|
+
|
|
128
|
+
if not isinstance(config, dict):
|
|
129
|
+
logger.warning(
|
|
130
|
+
"Invalid config format in %s: expected dict, got %s",
|
|
131
|
+
config_path, type(config).__name__
|
|
132
|
+
)
|
|
133
|
+
return {}
|
|
134
|
+
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
except json.JSONDecodeError as e:
|
|
138
|
+
logger.warning("Failed to parse config at %s: %s", config_path, e)
|
|
139
|
+
return {}
|
|
140
|
+
except OSError as e:
|
|
141
|
+
logger.warning("Failed to read config at %s: %s", config_path, e)
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _save_config(project_dir: Path, config: dict) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Save the project configuration to disk.
|
|
148
|
+
|
|
149
|
+
Creates the .autoforge directory if it doesn't exist.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
project_dir: Path to the project directory.
|
|
153
|
+
config: Configuration dictionary to save.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
OSError: If the file cannot be written.
|
|
157
|
+
"""
|
|
158
|
+
config_path = _get_config_path(project_dir)
|
|
159
|
+
|
|
160
|
+
# Ensure the .autoforge directory exists
|
|
161
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
165
|
+
json.dump(config, f, indent=2)
|
|
166
|
+
logger.debug("Saved config to %s", config_path)
|
|
167
|
+
except OSError as e:
|
|
168
|
+
logger.error("Failed to save config to %s: %s", config_path, e)
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# =============================================================================
|
|
173
|
+
# Project Type Detection
|
|
174
|
+
# =============================================================================
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _parse_package_json(project_dir: Path) -> dict | None:
|
|
178
|
+
"""
|
|
179
|
+
Parse package.json if it exists.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
project_dir: Path to the project directory.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Parsed package.json as dict, or None if not found or invalid.
|
|
186
|
+
"""
|
|
187
|
+
package_json_path = project_dir / "package.json"
|
|
188
|
+
|
|
189
|
+
if not package_json_path.exists():
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
with open(package_json_path, "r", encoding="utf-8") as f:
|
|
194
|
+
data = json.load(f)
|
|
195
|
+
if isinstance(data, dict):
|
|
196
|
+
return data
|
|
197
|
+
return None
|
|
198
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
199
|
+
logger.debug("Failed to parse package.json in %s: %s", project_dir, e)
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _is_poetry_project(project_dir: Path) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Check if pyproject.toml indicates a Poetry project.
|
|
206
|
+
|
|
207
|
+
Parses pyproject.toml to look for [tool.poetry] section.
|
|
208
|
+
Falls back to simple file existence check if tomllib is not available.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
project_dir: Path to the project directory.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
True if pyproject.toml exists and contains Poetry configuration.
|
|
215
|
+
"""
|
|
216
|
+
pyproject_path = project_dir / "pyproject.toml"
|
|
217
|
+
if not pyproject_path.exists():
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# If tomllib is available (Python 3.11+), parse and check for [tool.poetry]
|
|
221
|
+
if tomllib is not None:
|
|
222
|
+
try:
|
|
223
|
+
with open(pyproject_path, "rb") as f:
|
|
224
|
+
data = tomllib.load(f)
|
|
225
|
+
return "poetry" in data.get("tool", {})
|
|
226
|
+
except Exception:
|
|
227
|
+
# If parsing fails, fall back to False
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# Fallback for older Python: simple file existence check
|
|
231
|
+
# This is less accurate but provides backward compatibility
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def detect_project_type(project_dir: Path) -> str | None:
|
|
236
|
+
"""
|
|
237
|
+
Detect the project type by scanning for configuration files.
|
|
238
|
+
|
|
239
|
+
Detection priority (first match wins):
|
|
240
|
+
1. package.json with scripts.dev -> nodejs-vite
|
|
241
|
+
2. package.json with scripts.start -> nodejs-cra
|
|
242
|
+
3. pyproject.toml with [tool.poetry] -> python-poetry
|
|
243
|
+
4. manage.py -> python-django
|
|
244
|
+
5. requirements.txt + (main.py or app.py) -> python-fastapi
|
|
245
|
+
6. Cargo.toml -> rust
|
|
246
|
+
7. go.mod -> go
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
project_dir: Path to the project directory.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Project type string (e.g., "nodejs-vite", "python-django"),
|
|
253
|
+
or None if no known project type is detected.
|
|
254
|
+
"""
|
|
255
|
+
project_dir = Path(project_dir).resolve()
|
|
256
|
+
|
|
257
|
+
if not project_dir.exists() or not project_dir.is_dir():
|
|
258
|
+
logger.debug("Project directory does not exist: %s", project_dir)
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
# Check for Node.js projects (package.json)
|
|
262
|
+
package_json = _parse_package_json(project_dir)
|
|
263
|
+
if package_json is not None:
|
|
264
|
+
scripts = package_json.get("scripts", {})
|
|
265
|
+
if isinstance(scripts, dict):
|
|
266
|
+
# Check for 'dev' script first (typical for Vite, Next.js, etc.)
|
|
267
|
+
if "dev" in scripts:
|
|
268
|
+
logger.debug("Detected nodejs-vite project in %s", project_dir)
|
|
269
|
+
return "nodejs-vite"
|
|
270
|
+
# Fall back to 'start' script (typical for CRA)
|
|
271
|
+
if "start" in scripts:
|
|
272
|
+
logger.debug("Detected nodejs-cra project in %s", project_dir)
|
|
273
|
+
return "nodejs-cra"
|
|
274
|
+
|
|
275
|
+
# Check for Python Poetry project (must have [tool.poetry] in pyproject.toml)
|
|
276
|
+
if _is_poetry_project(project_dir):
|
|
277
|
+
logger.debug("Detected python-poetry project in %s", project_dir)
|
|
278
|
+
return "python-poetry"
|
|
279
|
+
|
|
280
|
+
# Check for Django project
|
|
281
|
+
if (project_dir / "manage.py").exists():
|
|
282
|
+
logger.debug("Detected python-django project in %s", project_dir)
|
|
283
|
+
return "python-django"
|
|
284
|
+
|
|
285
|
+
# Check for Python FastAPI project (requirements.txt + main.py or app.py)
|
|
286
|
+
if (project_dir / "requirements.txt").exists():
|
|
287
|
+
has_main = (project_dir / "main.py").exists()
|
|
288
|
+
has_app = (project_dir / "app.py").exists()
|
|
289
|
+
if has_main or has_app:
|
|
290
|
+
logger.debug("Detected python-fastapi project in %s", project_dir)
|
|
291
|
+
return "python-fastapi"
|
|
292
|
+
|
|
293
|
+
# Check for Rust project
|
|
294
|
+
if (project_dir / "Cargo.toml").exists():
|
|
295
|
+
logger.debug("Detected rust project in %s", project_dir)
|
|
296
|
+
return "rust"
|
|
297
|
+
|
|
298
|
+
# Check for Go project
|
|
299
|
+
if (project_dir / "go.mod").exists():
|
|
300
|
+
logger.debug("Detected go project in %s", project_dir)
|
|
301
|
+
return "go"
|
|
302
|
+
|
|
303
|
+
logger.debug("No known project type detected in %s", project_dir)
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# =============================================================================
|
|
308
|
+
# Dev Command Functions
|
|
309
|
+
# =============================================================================
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def get_default_dev_command(project_dir: Path) -> str | None:
|
|
313
|
+
"""
|
|
314
|
+
Get the auto-detected dev command for a project.
|
|
315
|
+
|
|
316
|
+
This returns the default command based on detected project type,
|
|
317
|
+
ignoring any custom command that may be configured.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
project_dir: Path to the project directory.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Default dev command string for the detected project type,
|
|
324
|
+
or None if no project type is detected.
|
|
325
|
+
"""
|
|
326
|
+
project_type = detect_project_type(project_dir)
|
|
327
|
+
|
|
328
|
+
if project_type is None:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
return PROJECT_TYPE_COMMANDS.get(project_type)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_dev_command(project_dir: Path) -> str | None:
|
|
335
|
+
"""
|
|
336
|
+
Get the effective dev command for a project.
|
|
337
|
+
|
|
338
|
+
Returns the custom command if one is configured,
|
|
339
|
+
otherwise returns the auto-detected default command.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
project_dir: Path to the project directory.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
The effective dev command (custom if set, else detected),
|
|
346
|
+
or None if neither is available.
|
|
347
|
+
"""
|
|
348
|
+
project_dir = Path(project_dir).resolve()
|
|
349
|
+
|
|
350
|
+
# Check for custom command first
|
|
351
|
+
config = _load_config(project_dir)
|
|
352
|
+
custom_command = config.get("dev_command")
|
|
353
|
+
|
|
354
|
+
if custom_command and isinstance(custom_command, str):
|
|
355
|
+
# Type is narrowed to str by isinstance check
|
|
356
|
+
result: str = custom_command
|
|
357
|
+
return result
|
|
358
|
+
|
|
359
|
+
# Fall back to auto-detected command
|
|
360
|
+
return get_default_dev_command(project_dir)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def set_dev_command(project_dir: Path, command: str) -> None:
|
|
364
|
+
"""
|
|
365
|
+
Save a custom dev command for a project.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
project_dir: Path to the project directory.
|
|
369
|
+
command: The custom dev command to save.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
ValueError: If command is empty or not a string, or if project_dir is invalid.
|
|
373
|
+
OSError: If the config file cannot be written.
|
|
374
|
+
"""
|
|
375
|
+
if not command or not isinstance(command, str):
|
|
376
|
+
raise ValueError("Command must be a non-empty string")
|
|
377
|
+
|
|
378
|
+
project_dir = _validate_project_dir(project_dir)
|
|
379
|
+
|
|
380
|
+
# Load existing config and update
|
|
381
|
+
config = _load_config(project_dir)
|
|
382
|
+
config["dev_command"] = command
|
|
383
|
+
|
|
384
|
+
_save_config(project_dir, config)
|
|
385
|
+
logger.info("Set custom dev command for %s: %s", project_dir.name, command)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def clear_dev_command(project_dir: Path) -> None:
|
|
389
|
+
"""
|
|
390
|
+
Remove the custom dev command, reverting to auto-detection.
|
|
391
|
+
|
|
392
|
+
If no config file exists or no custom command is set,
|
|
393
|
+
this function does nothing (no error is raised).
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
project_dir: Path to the project directory.
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
ValueError: If project_dir is not a valid directory.
|
|
400
|
+
"""
|
|
401
|
+
project_dir = _validate_project_dir(project_dir)
|
|
402
|
+
config_path = _get_config_path(project_dir)
|
|
403
|
+
|
|
404
|
+
if not config_path.exists():
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
config = _load_config(project_dir)
|
|
408
|
+
|
|
409
|
+
if "dev_command" not in config:
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
del config["dev_command"]
|
|
413
|
+
|
|
414
|
+
# If config is now empty, delete the file
|
|
415
|
+
if not config:
|
|
416
|
+
try:
|
|
417
|
+
config_path.unlink(missing_ok=True)
|
|
418
|
+
logger.info("Removed empty config file for %s", project_dir.name)
|
|
419
|
+
|
|
420
|
+
# Also remove .autoforge directory if empty
|
|
421
|
+
autoforge_dir = config_path.parent
|
|
422
|
+
if autoforge_dir.exists() and not any(autoforge_dir.iterdir()):
|
|
423
|
+
autoforge_dir.rmdir()
|
|
424
|
+
logger.debug("Removed empty .autoforge directory for %s", project_dir.name)
|
|
425
|
+
except OSError as e:
|
|
426
|
+
logger.warning("Failed to clean up config for %s: %s", project_dir.name, e)
|
|
427
|
+
else:
|
|
428
|
+
_save_config(project_dir, config)
|
|
429
|
+
|
|
430
|
+
logger.info("Cleared custom dev command for %s", project_dir.name)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def get_project_config(project_dir: Path) -> ProjectConfig:
|
|
434
|
+
"""
|
|
435
|
+
Get the full project configuration including detection results.
|
|
436
|
+
|
|
437
|
+
This provides all relevant configuration information in a single call,
|
|
438
|
+
useful for displaying in a UI or debugging.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
project_dir: Path to the project directory.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
ProjectConfig dict with:
|
|
445
|
+
- detected_type: The auto-detected project type (or None)
|
|
446
|
+
- detected_command: The default command for detected type (or None)
|
|
447
|
+
- custom_command: The user-configured custom command (or None)
|
|
448
|
+
- effective_command: The command that would actually be used (or None)
|
|
449
|
+
|
|
450
|
+
Raises:
|
|
451
|
+
ValueError: If project_dir is not a valid directory.
|
|
452
|
+
"""
|
|
453
|
+
project_dir = _validate_project_dir(project_dir)
|
|
454
|
+
|
|
455
|
+
# Detect project type and get default command
|
|
456
|
+
detected_type = detect_project_type(project_dir)
|
|
457
|
+
detected_command = PROJECT_TYPE_COMMANDS.get(detected_type) if detected_type else None
|
|
458
|
+
|
|
459
|
+
# Load custom command from config
|
|
460
|
+
config = _load_config(project_dir)
|
|
461
|
+
custom_command = config.get("dev_command")
|
|
462
|
+
|
|
463
|
+
# Validate custom_command is a string
|
|
464
|
+
if not isinstance(custom_command, str):
|
|
465
|
+
custom_command = None
|
|
466
|
+
|
|
467
|
+
# Determine effective command
|
|
468
|
+
effective_command = custom_command if custom_command else detected_command
|
|
469
|
+
|
|
470
|
+
return ProjectConfig(
|
|
471
|
+
detected_type=detected_type,
|
|
472
|
+
detected_command=detected_command,
|
|
473
|
+
custom_command=custom_command,
|
|
474
|
+
effective_command=effective_command,
|
|
475
|
+
)
|