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 @@
|
|
|
1
|
+
# Server utilities
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Process Utilities
|
|
3
|
+
=================
|
|
4
|
+
|
|
5
|
+
Shared utilities for process management across the codebase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import subprocess
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
import psutil
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class KillResult:
|
|
20
|
+
"""Result of a process tree kill operation.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
status: "success" if all processes terminated, "partial" if some required
|
|
24
|
+
force-kill, "failure" if parent couldn't be killed
|
|
25
|
+
parent_pid: PID of the parent process
|
|
26
|
+
children_found: Number of child processes found
|
|
27
|
+
children_terminated: Number of children that terminated gracefully
|
|
28
|
+
children_killed: Number of children that required SIGKILL
|
|
29
|
+
parent_forcekilled: Whether the parent required SIGKILL
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
status: Literal["success", "partial", "failure"]
|
|
33
|
+
parent_pid: int
|
|
34
|
+
children_found: int = 0
|
|
35
|
+
children_terminated: int = 0
|
|
36
|
+
children_killed: int = 0
|
|
37
|
+
parent_forcekilled: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def kill_process_tree(proc: subprocess.Popen, timeout: float = 5.0) -> KillResult:
|
|
41
|
+
"""Kill a process and all its child processes.
|
|
42
|
+
|
|
43
|
+
On Windows, subprocess.terminate() only kills the immediate process, leaving
|
|
44
|
+
orphaned child processes (e.g., spawned browser instances, coding/testing agents).
|
|
45
|
+
This function uses psutil to kill the entire process tree.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
proc: The subprocess.Popen object to kill
|
|
49
|
+
timeout: Seconds to wait for graceful termination before force-killing
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
KillResult with status and statistics about the termination
|
|
53
|
+
"""
|
|
54
|
+
result = KillResult(status="success", parent_pid=proc.pid)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
parent = psutil.Process(proc.pid)
|
|
58
|
+
# Get all children recursively before terminating
|
|
59
|
+
children = parent.children(recursive=True)
|
|
60
|
+
result.children_found = len(children)
|
|
61
|
+
|
|
62
|
+
logger.debug(
|
|
63
|
+
"Killing process tree: PID %d with %d children",
|
|
64
|
+
proc.pid, len(children)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Terminate children first (graceful)
|
|
68
|
+
for child in children:
|
|
69
|
+
try:
|
|
70
|
+
logger.debug("Terminating child PID %d (%s)", child.pid, child.name())
|
|
71
|
+
child.terminate()
|
|
72
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
73
|
+
# NoSuchProcess: already dead
|
|
74
|
+
# AccessDenied: Windows can raise this for system processes or already-exited processes
|
|
75
|
+
logger.debug("Child PID %d already gone or inaccessible: %s", child.pid, e)
|
|
76
|
+
|
|
77
|
+
# Wait for children to terminate
|
|
78
|
+
gone, still_alive = psutil.wait_procs(children, timeout=timeout)
|
|
79
|
+
result.children_terminated = len(gone)
|
|
80
|
+
|
|
81
|
+
logger.debug(
|
|
82
|
+
"Children after graceful wait: %d terminated, %d still alive",
|
|
83
|
+
len(gone), len(still_alive)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Force kill any remaining children
|
|
87
|
+
for child in still_alive:
|
|
88
|
+
try:
|
|
89
|
+
logger.debug("Force-killing child PID %d", child.pid)
|
|
90
|
+
child.kill()
|
|
91
|
+
result.children_killed += 1
|
|
92
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
93
|
+
logger.debug("Child PID %d gone during force-kill: %s", child.pid, e)
|
|
94
|
+
|
|
95
|
+
if result.children_killed > 0:
|
|
96
|
+
result.status = "partial"
|
|
97
|
+
|
|
98
|
+
# Now terminate the parent
|
|
99
|
+
logger.debug("Terminating parent PID %d", proc.pid)
|
|
100
|
+
proc.terminate()
|
|
101
|
+
try:
|
|
102
|
+
proc.wait(timeout=timeout)
|
|
103
|
+
logger.debug("Parent PID %d terminated gracefully", proc.pid)
|
|
104
|
+
except subprocess.TimeoutExpired:
|
|
105
|
+
logger.debug("Parent PID %d did not terminate, force-killing", proc.pid)
|
|
106
|
+
proc.kill()
|
|
107
|
+
proc.wait()
|
|
108
|
+
result.parent_forcekilled = True
|
|
109
|
+
result.status = "partial"
|
|
110
|
+
|
|
111
|
+
logger.debug(
|
|
112
|
+
"Process tree kill complete: status=%s, children=%d (terminated=%d, killed=%d)",
|
|
113
|
+
result.status, result.children_found,
|
|
114
|
+
result.children_terminated, result.children_killed
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
|
118
|
+
# NoSuchProcess: Process already dead
|
|
119
|
+
# AccessDenied: Windows can raise this for protected/system processes
|
|
120
|
+
# In either case, just ensure cleanup
|
|
121
|
+
logger.debug("Parent PID %d inaccessible (%s), attempting direct cleanup", proc.pid, e)
|
|
122
|
+
try:
|
|
123
|
+
proc.terminate()
|
|
124
|
+
proc.wait(timeout=1)
|
|
125
|
+
logger.debug("Direct termination of PID %d succeeded", proc.pid)
|
|
126
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
127
|
+
try:
|
|
128
|
+
proc.kill()
|
|
129
|
+
logger.debug("Direct force-kill of PID %d succeeded", proc.pid)
|
|
130
|
+
except OSError as kill_error:
|
|
131
|
+
logger.debug("Direct force-kill of PID %d failed: %s", proc.pid, kill_error)
|
|
132
|
+
result.status = "failure"
|
|
133
|
+
|
|
134
|
+
return result
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Helper Utilities
|
|
3
|
+
========================
|
|
4
|
+
|
|
5
|
+
Shared project path lookup used across all server routers and websocket handlers.
|
|
6
|
+
Consolidates the previously duplicated _get_project_path() function.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Ensure the project root is on sys.path so `registry` can be imported.
|
|
13
|
+
# This is necessary because `registry.py` lives at the repository root,
|
|
14
|
+
# outside the `server` package.
|
|
15
|
+
_root = Path(__file__).parent.parent.parent
|
|
16
|
+
if str(_root) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_root))
|
|
18
|
+
|
|
19
|
+
from registry import get_project_path as _registry_get_project_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_project_path(project_name: str) -> Path | None:
|
|
23
|
+
"""Look up a project's filesystem path from the global registry.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_name: The registered name of the project.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The resolved ``Path`` to the project directory, or ``None`` if the
|
|
30
|
+
project is not found in the registry.
|
|
31
|
+
"""
|
|
32
|
+
return _registry_get_project_path(project_name)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared Validation Utilities
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
Project name validation used across REST endpoints and WebSocket handlers.
|
|
6
|
+
Two variants are provided:
|
|
7
|
+
|
|
8
|
+
* ``is_valid_project_name`` -- returns ``bool``, suitable for WebSocket
|
|
9
|
+
handlers where raising an HTTPException is not appropriate.
|
|
10
|
+
* ``validate_project_name`` -- raises ``HTTPException(400)`` on failure,
|
|
11
|
+
suitable for REST endpoint handlers.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
from fastapi import HTTPException
|
|
17
|
+
|
|
18
|
+
# Compiled once; reused by both variants.
|
|
19
|
+
_PROJECT_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]{1,50}$')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_valid_project_name(name: str) -> bool:
|
|
23
|
+
"""Check whether *name* is a valid project name.
|
|
24
|
+
|
|
25
|
+
Allows only ASCII letters, digits, hyphens, and underscores (1-50 chars).
|
|
26
|
+
Returns ``True`` if valid, ``False`` otherwise.
|
|
27
|
+
|
|
28
|
+
Use this in WebSocket handlers where you need to close the socket
|
|
29
|
+
yourself rather than raise an HTTP error.
|
|
30
|
+
"""
|
|
31
|
+
return bool(_PROJECT_NAME_RE.match(name))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_project_name(name: str) -> str:
|
|
35
|
+
"""Validate and return *name*, or raise ``HTTPException(400)``.
|
|
36
|
+
|
|
37
|
+
Suitable for REST endpoint handlers where FastAPI will convert the
|
|
38
|
+
exception into an HTTP 400 response automatically.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Project name to validate.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The validated project name (unchanged).
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
HTTPException: If *name* is invalid.
|
|
48
|
+
"""
|
|
49
|
+
if not _PROJECT_NAME_RE.match(name):
|
|
50
|
+
raise HTTPException(
|
|
51
|
+
status_code=400,
|
|
52
|
+
detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
|
|
53
|
+
)
|
|
54
|
+
return name
|