mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
add local.sh Windows compatability
This commit is contained in:
+3
-1
@@ -15,4 +15,6 @@ apps/db/snips/*
|
||||
!apps/db/snips/.gitkeep
|
||||
|
||||
# ignore everything in data/
|
||||
data/**
|
||||
data/**
|
||||
|
||||
.venv/
|
||||
@@ -47,7 +47,7 @@ async def serve_app_file(app_id: str, filepath: str):
|
||||
raise HTTPException(status_code=403, detail="Path traversal not allowed")
|
||||
if not os.path.isfile(full_path):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
with open(full_path) as f:
|
||||
with open(full_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
mime, _ = mimetypes.guess_type(filepath)
|
||||
return Response(content=content, media_type=mime or "text/plain")
|
||||
@@ -61,7 +61,7 @@ async def serve_app_file(app_id: str, filepath: str):
|
||||
raise HTTPException(status_code=403, detail="Path traversal not allowed")
|
||||
if not os.path.isfile(full_path):
|
||||
raise HTTPException(status_code=404, detail="File not found in app")
|
||||
with open(full_path) as f:
|
||||
with open(full_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
mime, _ = mimetypes.guess_type(filepath)
|
||||
return Response(content=content, media_type=mime or "text/plain")
|
||||
@@ -101,17 +101,17 @@ async def seed_app(body: OpenSwarmAppSeedRequest):
|
||||
if not full_path.startswith(os.path.normpath(folder)):
|
||||
continue
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "w") as f:
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
else:
|
||||
for rel_path, content in APP_BUILDER_TEMPLATE_FILES.items():
|
||||
full_path = os.path.join(folder, rel_path)
|
||||
with open(full_path, "w") as f:
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
with open(os.path.join(folder, "SKILL.md"), "w") as f:
|
||||
with open(os.path.join(folder, "SKILL.md"), "w", encoding="utf-8") as f:
|
||||
f.write(APP_BUILDER_SKILL)
|
||||
if body.meta:
|
||||
with open(os.path.join(folder, "meta.json"), "w") as f:
|
||||
with open(os.path.join(folder, "meta.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(body.meta, f, indent=2)
|
||||
return {"path": os.path.abspath(folder)}
|
||||
|
||||
@@ -125,7 +125,7 @@ async def write_app_file(app_id: str, filepath: str, body: dict):
|
||||
if not full_path.startswith(os.path.normpath(folder)):
|
||||
raise HTTPException(status_code=403, detail="Path traversal not allowed")
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "w") as f:
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(body.get("content", ""))
|
||||
return {"ok": True}
|
||||
|
||||
@@ -186,9 +186,9 @@ async def create_app(body: OpenSwarmAppCreate):
|
||||
if not full_path.startswith(os.path.normpath(folder)):
|
||||
continue
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "w") as f:
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
with open(os.path.join(folder, "SKILL.md"), "w") as f:
|
||||
with open(os.path.join(folder, "SKILL.md"), "w", encoding="utf-8") as f:
|
||||
f.write(APP_BUILDER_SKILL)
|
||||
P_APP_METADATA_STORE.save(app)
|
||||
return {"ok": True, "app": app.model_dump()}
|
||||
@@ -239,7 +239,7 @@ def _read_app_file(app_id: str, filename: str) -> Optional[str]:
|
||||
path = os.path.join(APP_BUILDER_CONTENT_DIR, app_id, filename)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
with open(path) as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
@app_builder.router.post("/execute")
|
||||
|
||||
@@ -9,7 +9,7 @@ P_CONTENT_DIR = os.path.join(P_SELF_DIR, "content")
|
||||
|
||||
@typechecked
|
||||
def p_read(filename: str) -> str:
|
||||
with open(os.path.join(P_CONTENT_DIR, filename)) as f:
|
||||
with open(os.path.join(P_CONTENT_DIR, filename), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
APP_BUILDER_SKILL: str = p_read("app_builder_skill.md")
|
||||
|
||||
@@ -12,7 +12,7 @@ def walk_directory(folder: str) -> Dict[str, str]:
|
||||
full_path = os.path.join(root, fname)
|
||||
rel_path = os.path.relpath(full_path, folder)
|
||||
try:
|
||||
with open(full_path) as f:
|
||||
with open(full_path, encoding="utf-8") as f:
|
||||
files[rel_path] = f.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -168,7 +168,7 @@ async def duplicate_dashboard(dashboard_id: str):
|
||||
"browser_cards": source_data.get("layout", {}).get("browser_cards", {}),
|
||||
},
|
||||
}
|
||||
with open(os.path.join(DB_ROOT, "dashboards", f"{new_id}.json"), "w") as f:
|
||||
with open(os.path.join(DB_ROOT, "dashboards", f"{new_id}.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(new_dashboard, f, indent=2)
|
||||
|
||||
return new_dashboard
|
||||
|
||||
@@ -22,7 +22,7 @@ settings = SubApp("settings", settings_lifespan)
|
||||
def load_settings() -> AppSettings:
|
||||
"""Load settings from JSON file, returning defaults if not found."""
|
||||
if os.path.exists(SETTINGS_FILE):
|
||||
with open(SETTINGS_FILE) as f:
|
||||
with open(SETTINGS_FILE, encoding="utf-8") as f:
|
||||
settings = AppSettings(**json.load(f))
|
||||
if settings.default_system_prompt is None:
|
||||
settings.default_system_prompt = DEFAULT_SYSTEM_PROMPT
|
||||
@@ -38,7 +38,7 @@ async def get_settings():
|
||||
@settings.router.put("/update_settings")
|
||||
async def update_settings(body: AppSettings):
|
||||
os.makedirs(SETTINGS_DIR, exist_ok=True)
|
||||
with open(SETTINGS_FILE, "w") as f:
|
||||
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(body.model_dump(), f, indent=2)
|
||||
return {"ok": True, "settings": body.model_dump()}
|
||||
|
||||
@@ -48,7 +48,7 @@ async def reset_system_prompt():
|
||||
current = load_settings()
|
||||
current.default_system_prompt = DEFAULT_SYSTEM_PROMPT
|
||||
os.makedirs(SETTINGS_DIR, exist_ok=True)
|
||||
with open(SETTINGS_FILE, "w") as f:
|
||||
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(current.model_dump(), f, indent=2)
|
||||
return {"ok": True, "settings": current.model_dump()}
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ class SkillStore(BaseModel):
|
||||
@typechecked
|
||||
def p_load_index(self) -> Dict[str, dict]:
|
||||
if os.path.exists(self.index_path):
|
||||
with open(self.index_path) as f:
|
||||
with open(self.index_path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
@typechecked
|
||||
def p_save_index(self, index: Dict[str, dict]) -> None:
|
||||
with open(self.index_path, "w") as f:
|
||||
with open(self.index_path, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, indent=2)
|
||||
|
||||
@staticmethod
|
||||
@@ -50,7 +50,7 @@ class SkillStore(BaseModel):
|
||||
if not fname.endswith(".md"):
|
||||
continue
|
||||
fpath = os.path.join(self.skills_dir, fname)
|
||||
with open(fpath) as f:
|
||||
with open(fpath, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
skill_id = fname.removesuffix(".md")
|
||||
meta = index.get(skill_id, {})
|
||||
@@ -75,7 +75,7 @@ class SkillStore(BaseModel):
|
||||
def create(self, name: str, description: str, content: str, command: str = "") -> Skill:
|
||||
slug = self.slug(name)
|
||||
fpath = self.p_skill_path(slug)
|
||||
with open(fpath, "w") as f:
|
||||
with open(fpath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
index = self.p_load_index()
|
||||
index[slug] = {"name": name, "description": description, "command": command or slug}
|
||||
@@ -91,7 +91,7 @@ class SkillStore(BaseModel):
|
||||
if not os.path.exists(fpath):
|
||||
raise FileNotFoundError(skill_id)
|
||||
if content is not None:
|
||||
with open(fpath, "w") as f:
|
||||
with open(fpath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
index = self.p_load_index()
|
||||
meta = index.get(skill_id, {})
|
||||
@@ -103,7 +103,7 @@ class SkillStore(BaseModel):
|
||||
meta["command"] = command
|
||||
index[skill_id] = meta
|
||||
self.p_save_index(index)
|
||||
with open(fpath) as f:
|
||||
with open(fpath, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
return Skill(id=skill_id, name=meta.get("name", skill_id),
|
||||
description=meta.get("description", ""),
|
||||
|
||||
@@ -67,14 +67,14 @@ async def read_skill_workspace(workspace_id: str):
|
||||
skill_content: Optional[str] = None
|
||||
skill_path = os.path.join(folder, "SKILL.md")
|
||||
if os.path.isfile(skill_path):
|
||||
with open(skill_path) as f:
|
||||
with open(skill_path, encoding="utf-8") as f:
|
||||
skill_content = f.read()
|
||||
|
||||
meta: Optional[dict] = None
|
||||
meta_path = os.path.join(folder, "meta.json")
|
||||
if os.path.isfile(meta_path):
|
||||
try:
|
||||
with open(meta_path) as f:
|
||||
with open(meta_path, encoding="utf-8") as f:
|
||||
meta = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
@@ -95,10 +95,10 @@ async def seed_skill_workspace(body: _WorkspaceSeedBody):
|
||||
folder = os.path.join(SKILLS_WORKSPACE_DIR, body.workspace_id)
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
if body.skill_content:
|
||||
with open(os.path.join(folder, "SKILL.md"), "w") as f:
|
||||
with open(os.path.join(folder, "SKILL.md"), "w", encoding="utf-8") as f:
|
||||
f.write(body.skill_content)
|
||||
if body.meta:
|
||||
with open(os.path.join(folder, "meta.json"), "w") as f:
|
||||
with open(os.path.join(folder, "meta.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(body.meta, f, indent=2)
|
||||
return {"path": os.path.abspath(folder)}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp import ClientSession
|
||||
from mcp.types import Implementation
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
from backend.apps.tools.discover_tools.DiscoveryError import DiscoveryError
|
||||
from typeguard import typechecked
|
||||
|
||||
@@ -65,13 +65,13 @@ async def list_builtin_tools() -> dict:
|
||||
def load_builtin_permissions() -> dict[str, TOOL_PERMISSIONS]:
|
||||
if not os.path.exists(BUILTIN_PERMS_PATH):
|
||||
return {}
|
||||
with open(BUILTIN_PERMS_PATH) as f:
|
||||
with open(BUILTIN_PERMS_PATH, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_builtin_permissions(perms: dict[str, str]) -> None:
|
||||
os.makedirs(os.path.dirname(BUILTIN_PERMS_PATH), exist_ok=True)
|
||||
with open(BUILTIN_PERMS_PATH, "w") as f:
|
||||
with open(BUILTIN_PERMS_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(perms, f, indent=2)
|
||||
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class PydanticStore(BaseModel, Generic[T]):
|
||||
path = os.path.join(self.data_dir, fname)
|
||||
size = os.path.getsize(path)
|
||||
debug("load_all: loading %s (%s bytes)", fname, size, table=False)
|
||||
with open(path) as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
debug("load_all: raw content length=%s, first 100 chars: %s", len(raw), raw[:100], table=False)
|
||||
result.append(self.model_cls(**json.loads(raw)))
|
||||
@@ -62,7 +62,7 @@ class PydanticStore(BaseModel, Generic[T]):
|
||||
path = self.p_path(item_id)
|
||||
debug("save: writing %s to %s", item_id, path, table=False)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", dir=self.data_dir, suffix=".tmp", delete=False
|
||||
"w", dir=self.data_dir, suffix=".tmp", delete=False, encoding="utf-8"
|
||||
) as tmp:
|
||||
json.dump(self.p_dump(item), tmp, indent=2)
|
||||
tmp_path = tmp.name
|
||||
@@ -74,7 +74,7 @@ class PydanticStore(BaseModel, Generic[T]):
|
||||
path = self.p_path(item_id)
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=404, detail=self.not_found_detail)
|
||||
with open(path) as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return self.model_cls(**json.load(f))
|
||||
|
||||
@typechecked
|
||||
@@ -82,7 +82,7 @@ class PydanticStore(BaseModel, Generic[T]):
|
||||
path = self.p_path(item_id)
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
with open(path) as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return self.model_cls(**json.load(f))
|
||||
|
||||
@typechecked
|
||||
|
||||
@@ -55,7 +55,7 @@ def resolve_context_paths(context_paths: List[ContextPath]) -> str:
|
||||
continue
|
||||
if cp_type == "file" and os.path.isfile(path):
|
||||
try:
|
||||
with open(path, "r", errors="replace") as f:
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
||||
content: str = f.read(MAX_FILE_READ_BYTES)
|
||||
sections.append(
|
||||
f'<context_file path="{path}">\n{content}\n</context_file>'
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import json
|
||||
import os
|
||||
|
||||
_config_path = os.path.join(os.path.dirname(__file__), "..", "ports.config.json")
|
||||
with open(_config_path) as _f:
|
||||
with open(_config_path, encoding="utf-8") as _f:
|
||||
_cfg = json.load(_f)
|
||||
|
||||
BACKEND_DEV_PORT: int = _cfg["backend"]["dev"]
|
||||
|
||||
+10
-11
@@ -1,16 +1,15 @@
|
||||
#!/bin/bash
|
||||
# The comment above is shebang, DO NOT REMOVE
|
||||
DEV_ABSPATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' 's/\r//g' "$DEV_ABSPATH"
|
||||
else
|
||||
sed -i 's/\r//g' "$DEV_ABSPATH"
|
||||
fi
|
||||
BACKEND_DIR_ABSPATH="$(dirname "$DEV_ABSPATH")"
|
||||
PROJECT_ROOT_ABSPATH="$(dirname "$BACKEND_DIR_ABSPATH")"
|
||||
|
||||
# shellcheck source=../run/utils/platform.sh
|
||||
source "$PROJECT_ROOT_ABSPATH/run/utils/platform.sh"
|
||||
ensure_lf "$DEV_ABSPATH"
|
||||
chmod +x "$DEV_ABSPATH"
|
||||
|
||||
PROJECT_ROOT_ABSPATH="$(dirname "$(dirname "$DEV_ABSPATH")")"
|
||||
BACKEND_DIR_ABSPATH="$PROJECT_ROOT_ABSPATH/backend"
|
||||
UV_BIN="$BACKEND_DIR_ABSPATH/uv-bin/uv"
|
||||
UV_BIN="$BACKEND_DIR_ABSPATH/uv-bin/uv${EXE_EXT}"
|
||||
|
||||
cleanup() {
|
||||
echo "Shutting down..."
|
||||
@@ -32,8 +31,8 @@ if [[ $? -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Read dev port from ports.config.json ---
|
||||
BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT_ABSPATH/ports.config.json'))['backend']['dev'])")
|
||||
# --- Read dev port from ports.config.json (path via argv so MSYS converts it) ---
|
||||
BACKEND_PORT=$("$PY" -c "import json,sys; print(json.load(open(sys.argv[1], encoding='utf-8'))['backend']['dev'])" "$PROJECT_ROOT_ABSPATH/ports.config.json")
|
||||
|
||||
# --- Start the backend server ---
|
||||
echo "Starting backend server on http://0.0.0.0:${BACKEND_PORT} ..."
|
||||
@@ -41,4 +40,4 @@ cd "$PROJECT_ROOT_ABSPATH"
|
||||
WATCHFILES_FORCE_POLLING=true "$UV_BIN" run --project "$BACKEND_DIR_ABSPATH" python -m uvicorn backend.main:app \
|
||||
--host 0.0.0.0 --port "$BACKEND_PORT" --reload \
|
||||
--reload-dir "$BACKEND_DIR_ABSPATH" \
|
||||
--reload-exclude '*.pyc'
|
||||
--reload-exclude '*.pyc'
|
||||
|
||||
+7
-15
@@ -1,20 +1,13 @@
|
||||
#!/bin/bash
|
||||
# The comment above is shebang, DO NOT REMOVE
|
||||
DEV_ABSPATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# echo "In macOS server sed START"
|
||||
# echo "SERVER_ABSPATH: $SERVER_ABSPATH"
|
||||
sed -i '' 's/\r//g' "$DEV_ABSPATH"
|
||||
# echo "In macOS server sed END"
|
||||
else
|
||||
# echo "NOT in macOS server START"
|
||||
# echo "SERVER_ABSPATH: $SERVER_ABSPATH"
|
||||
sed -i 's/\r//g' "$DEV_ABSPATH"
|
||||
# echo "NOT in macOS server START"
|
||||
fi
|
||||
chmod +x "$DEV_ABSPATH"
|
||||
|
||||
FRONTEND_DIR_ABSPATH="$(dirname "$DEV_ABSPATH")"
|
||||
PROJECT_ROOT_ABSPATH="$(dirname "$FRONTEND_DIR_ABSPATH")"
|
||||
|
||||
# shellcheck source=../run/utils/platform.sh
|
||||
source "$PROJECT_ROOT_ABSPATH/run/utils/platform.sh"
|
||||
ensure_lf "$DEV_ABSPATH"
|
||||
chmod +x "$DEV_ABSPATH"
|
||||
|
||||
echo "Installing dependencies..."
|
||||
cd "$FRONTEND_DIR_ABSPATH"
|
||||
@@ -23,5 +16,4 @@ npm install
|
||||
echo "Building with development mode..."
|
||||
npm run dev
|
||||
|
||||
# exit back to the dir that we were in before
|
||||
cd -
|
||||
cd -
|
||||
|
||||
@@ -5,12 +5,15 @@
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="${1:-$(dirname "$SCRIPT_DIR")}"
|
||||
|
||||
# shellcheck source=../run/utils/platform.sh
|
||||
source "$(dirname "$SCRIPT_DIR")/run/utils/platform.sh"
|
||||
|
||||
YELLOW='\033[33m'
|
||||
CYAN='\033[36m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
LINT_OUTPUT=$(python3 "$SCRIPT_DIR/lint.py" --root "$ROOT_DIR" 2>&1)
|
||||
LINT_OUTPUT=$("$PY" "$SCRIPT_DIR/lint.py" --root "$ROOT_DIR" 2>&1)
|
||||
LINT_EXIT=$?
|
||||
|
||||
if [ $LINT_EXIT -ne 0 ]; then
|
||||
|
||||
+60
-35
@@ -1,16 +1,14 @@
|
||||
#!/bin/bash
|
||||
# The comment above is shebang, DO NOT REMOVE
|
||||
SCRIPT_ABSPATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' 's/\r//g' "$SCRIPT_ABSPATH"
|
||||
else
|
||||
sed -i 's/\r//g' "$SCRIPT_ABSPATH"
|
||||
fi
|
||||
chmod +x "$SCRIPT_ABSPATH"
|
||||
|
||||
RUN_DIR_ROOT="$(dirname "$SCRIPT_ABSPATH")"
|
||||
PROJECT_ROOT="$(dirname "$RUN_DIR_ROOT")"
|
||||
|
||||
# shellcheck source=utils/platform.sh
|
||||
source "$RUN_DIR_ROOT/utils/platform.sh"
|
||||
ensure_lf "$SCRIPT_ABSPATH"
|
||||
chmod +x "$SCRIPT_ABSPATH"
|
||||
|
||||
BLUE='\033[0;34m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
@@ -23,16 +21,6 @@ FRONTEND_PID=""
|
||||
ELECTRON_PID=""
|
||||
SHUTTING_DOWN=false
|
||||
|
||||
kill_tree() {
|
||||
local pid=$1 sig=${2:-TERM}
|
||||
local children
|
||||
children=$(pgrep -P "$pid" 2>/dev/null)
|
||||
for child in $children; do
|
||||
kill_tree "$child" "$sig"
|
||||
done
|
||||
kill -"$sig" "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
$SHUTTING_DOWN && return
|
||||
SHUTTING_DOWN=true
|
||||
@@ -45,7 +33,13 @@ cleanup() {
|
||||
for pid in $ELECTRON_PID $FRONTEND_PID; do
|
||||
[[ -n "$pid" ]] && kill_tree "$pid" TERM
|
||||
done
|
||||
[[ -n "$BACKEND_PID" ]] && kill -TERM "$BACKEND_PID" 2>/dev/null
|
||||
if [[ -n "$BACKEND_PID" ]]; then
|
||||
if [[ "$IS_WINDOWS" == "true" ]]; then
|
||||
kill_tree "$BACKEND_PID" TERM
|
||||
else
|
||||
kill -TERM "$BACKEND_PID" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
local elapsed=0
|
||||
while (( elapsed < 10 )); do
|
||||
@@ -71,35 +65,63 @@ trap cleanup EXIT
|
||||
|
||||
# --- Ensure bundled uv/uvx for MCP servers ---
|
||||
UV_BIN_DIR="$PROJECT_ROOT/backend/uv-bin"
|
||||
if [ ! -f "$UV_BIN_DIR/uvx" ]; then
|
||||
if [ ! -f "$UV_BIN_DIR/uvx${EXE_EXT}" ]; then
|
||||
echo -e "${YELLOW}${BOLD}[uv]${RESET} Downloading uv/uvx..."
|
||||
mkdir -p "$UV_BIN_DIR"
|
||||
ARCH=$(uname -m)
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar xz -C /tmp
|
||||
cp /tmp/uv-aarch64-apple-darwin/uv "$UV_BIN_DIR/uv"
|
||||
cp /tmp/uv-aarch64-apple-darwin/uvx "$UV_BIN_DIR/uvx"
|
||||
# NOTE: do not name this `TMP` -- on Windows, `TMP` is an exported env var
|
||||
# and bash assignment preserves the export attribute, so child processes
|
||||
# (e.g. uv) would inherit it and crash once we `rm -rf` the directory.
|
||||
UV_DL_TMP=$(mktemp -d)
|
||||
if [[ "$IS_WINDOWS" == "true" ]]; then
|
||||
case "$ARCH" in
|
||||
aarch64|arm64) UV_PKG="uv-aarch64-pc-windows-msvc.zip" ;;
|
||||
*) UV_PKG="uv-x86_64-pc-windows-msvc.zip" ;;
|
||||
esac
|
||||
curl -fsSL "https://github.com/astral-sh/uv/releases/latest/download/${UV_PKG}" -o "$UV_DL_TMP/uv.zip"
|
||||
# Use Python's zipfile module: GNU tar in MSYS/Git Bash can't read zips,
|
||||
# and Windows' libarchive tar.exe is shadowed on PATH by GNU tar.
|
||||
# The Windows zip stores uv.exe/uvx.exe at the archive root (no wrapper dir),
|
||||
# unlike the Linux/macOS tarballs which have a uv-<triple>/ prefix.
|
||||
"$PY" -m zipfile -e "$(py_path "$UV_DL_TMP/uv.zip")" "$(py_path "$UV_DL_TMP")"
|
||||
cp "$UV_DL_TMP/uv.exe" "$UV_BIN_DIR/uv.exe"
|
||||
cp "$UV_DL_TMP/uvx.exe" "$UV_BIN_DIR/uvx.exe"
|
||||
elif [[ "$IS_MAC" == "true" ]]; then
|
||||
if [[ "$ARCH" == "arm64" ]]; then
|
||||
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar xz -C "$UV_DL_TMP"
|
||||
cp "$UV_DL_TMP/uv-aarch64-apple-darwin/uv" "$UV_BIN_DIR/uv"
|
||||
cp "$UV_DL_TMP/uv-aarch64-apple-darwin/uvx" "$UV_BIN_DIR/uvx"
|
||||
else
|
||||
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-apple-darwin.tar.gz" | tar xz -C "$UV_DL_TMP"
|
||||
cp "$UV_DL_TMP/uv-x86_64-apple-darwin/uv" "$UV_BIN_DIR/uv"
|
||||
cp "$UV_DL_TMP/uv-x86_64-apple-darwin/uvx" "$UV_BIN_DIR/uvx"
|
||||
fi
|
||||
chmod +x "$UV_BIN_DIR/uv" "$UV_BIN_DIR/uvx"
|
||||
else
|
||||
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-apple-darwin.tar.gz" | tar xz -C /tmp
|
||||
cp /tmp/uv-x86_64-apple-darwin/uv "$UV_BIN_DIR/uv"
|
||||
cp /tmp/uv-x86_64-apple-darwin/uvx "$UV_BIN_DIR/uvx"
|
||||
case "$ARCH" in
|
||||
aarch64|arm64) UV_PKG_BASE="uv-aarch64-unknown-linux-gnu" ;;
|
||||
*) UV_PKG_BASE="uv-x86_64-unknown-linux-gnu" ;;
|
||||
esac
|
||||
curl -sL "https://github.com/astral-sh/uv/releases/latest/download/${UV_PKG_BASE}.tar.gz" | tar xz -C "$UV_DL_TMP"
|
||||
cp "$UV_DL_TMP/$UV_PKG_BASE/uv" "$UV_BIN_DIR/uv"
|
||||
cp "$UV_DL_TMP/$UV_PKG_BASE/uvx" "$UV_BIN_DIR/uvx"
|
||||
chmod +x "$UV_BIN_DIR/uv" "$UV_BIN_DIR/uvx"
|
||||
fi
|
||||
chmod +x "$UV_BIN_DIR/uv" "$UV_BIN_DIR/uvx"
|
||||
rm -rf /tmp/uv-*-apple-darwin
|
||||
rm -rf "$UV_DL_TMP"
|
||||
fi
|
||||
|
||||
# --- Read ports from config ---
|
||||
BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT/ports.config.json'))['backend']['dev'])")
|
||||
FRONTEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT/ports.config.json'))['frontend']['dev'])")
|
||||
# --- Read ports from config (path passed via argv so MSYS converts it) ---
|
||||
BACKEND_PORT=$("$PY" -c "import json,sys; print(json.load(open(sys.argv[1]))['backend']['dev'])" "$PROJECT_ROOT/ports.config.json")
|
||||
FRONTEND_PORT=$("$PY" -c "import json,sys; print(json.load(open(sys.argv[1]))['frontend']['dev'])" "$PROJECT_ROOT/ports.config.json")
|
||||
|
||||
# --- Run structural linter (warnings only, non-blocking) ---
|
||||
LINT_OUTPUT=$(python3 "$PROJECT_ROOT/linter/lint.py" --root "$PROJECT_ROOT" 2>&1)
|
||||
LINT_OUTPUT=$("$PY" "$PROJECT_ROOT/linter/lint.py" --root "$PROJECT_ROOT" 2>&1)
|
||||
LINT_EXIT=$?
|
||||
if [ $LINT_EXIT -ne 0 ]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}${BOLD}[structlint] Violations found:${RESET}"
|
||||
echo "$LINT_OUTPUT" | grep -v "^structlint:" | while IFS= read -r line; do
|
||||
echo -e "${YELLOW} $line${RESET}"
|
||||
printf '%b %s%b\n' "$YELLOW" "$line" "$RESET"
|
||||
done
|
||||
LINT_COUNT=$(echo "$LINT_OUTPUT" | grep -oE '[0-9]+ error' | head -1 | grep -oE '[0-9]+')
|
||||
echo -e "${YELLOW}${BOLD} ${LINT_COUNT} violation(s) — fix or add exceptions in linter/config/config.json${RESET}"
|
||||
@@ -114,6 +136,7 @@ bash "$PROJECT_ROOT/backend/run.sh" > >(
|
||||
done
|
||||
) 2>&1 &
|
||||
BACKEND_PID=$!
|
||||
track_winpid "$BACKEND_PID"
|
||||
|
||||
# --- Wait for backend to become healthy ---
|
||||
echo -e "${YELLOW}${BOLD}Waiting for backend (http://localhost:${BACKEND_PORT}) to be ready...${RESET}"
|
||||
@@ -145,6 +168,7 @@ bash "$PROJECT_ROOT/frontend/run.sh" > >(
|
||||
done
|
||||
) 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
track_winpid "$FRONTEND_PID"
|
||||
|
||||
# --- Wait for frontend dev server to become available ---
|
||||
echo -e "${YELLOW}${BOLD}Waiting for frontend (http://localhost:${FRONTEND_PORT}) to be ready...${RESET}"
|
||||
@@ -177,8 +201,8 @@ if [ ! -d "$PROJECT_ROOT/electron/node_modules" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Sign Electron VMP for DRM (if EVS account exists) ---
|
||||
if [ -f "$PROJECT_ROOT/electron/scripts/sign-vmp.sh" ]; then
|
||||
# --- Sign Electron VMP for DRM (macOS only; Windows uses a different DRM flow) ---
|
||||
if [[ "$IS_WINDOWS" != "true" ]] && [ -f "$PROJECT_ROOT/electron/scripts/sign-vmp.sh" ]; then
|
||||
echo -e "${YELLOW}${BOLD}[vmp]${RESET} Checking VMP signature..."
|
||||
bash "$PROJECT_ROOT/electron/scripts/sign-vmp.sh" 2>&1 | while IFS= read -r line; do
|
||||
printf "${YELLOW}${BOLD}%s${RESET}\n" "$line"
|
||||
@@ -193,6 +217,7 @@ echo -e "${MAGENTA}${BOLD}[electron]${RESET} Launching Electron dev shell..."
|
||||
done
|
||||
) 2>&1 &
|
||||
ELECTRON_PID=$!
|
||||
track_winpid "$ELECTRON_PID"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}All services are running. Press Ctrl+C to stop.${RESET}"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# Cross-platform helpers for the dev shell scripts.
|
||||
# Sourced by: run/local.sh, backend/run.sh, frontend/run.sh, linter/print_errors.sh
|
||||
#
|
||||
# Exports:
|
||||
# IS_WINDOWS / IS_MAC / IS_LINUX ("true" or empty)
|
||||
# EXE_EXT (".exe" on Windows, "" elsewhere)
|
||||
# PY Working Python 3 interpreter command
|
||||
# ensure_lf <file> Strip CRLF from a file (BSD/GNU sed safe)
|
||||
# py_path <unix_path> Convert /c/... to C:/... on Windows
|
||||
# track_winpid <msys_pid> Snapshot Win32 pid for later taskkill
|
||||
# kill_tree <msys_pid> [SIG] Kill a process tree (taskkill on Win)
|
||||
|
||||
case "$OSTYPE" in
|
||||
msys*|cygwin*|mingw*) IS_WINDOWS=true; EXE_EXT=".exe" ;;
|
||||
darwin*) IS_MAC=true; EXE_EXT="" ;;
|
||||
*) IS_LINUX=true; EXE_EXT="" ;;
|
||||
esac
|
||||
|
||||
# Verify a candidate command actually runs Python 3. Rejects the Microsoft
|
||||
# Store python.exe / python3.exe stubs, which exit 0 on --version but print
|
||||
# nothing (and would silently open the Store on real invocation).
|
||||
_is_real_python3() {
|
||||
local out
|
||||
out=$("$@" --version 2>&1) || return 1
|
||||
[[ "$out" == Python\ 3.* ]]
|
||||
}
|
||||
|
||||
PY=""
|
||||
if [[ "$IS_WINDOWS" == "true" ]]; then
|
||||
_PY_CANDIDATES=("python" "py -3" "python3")
|
||||
else
|
||||
_PY_CANDIDATES=("python3" "python")
|
||||
fi
|
||||
for _c in "${_PY_CANDIDATES[@]}"; do
|
||||
if _is_real_python3 $_c; then
|
||||
PY="$_c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
unset _c _PY_CANDIDATES
|
||||
if [[ -z "$PY" ]]; then
|
||||
echo "ERROR: no working Python 3 interpreter found in PATH" >&2
|
||||
echo " Install Python 3 from https://www.python.org/downloads/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_lf() {
|
||||
local f=$1
|
||||
if [[ "$IS_MAC" == "true" ]]; then
|
||||
sed -i '' 's/\r//g' "$f"
|
||||
else
|
||||
sed -i 's/\r//g' "$f"
|
||||
fi
|
||||
}
|
||||
|
||||
py_path() {
|
||||
if [[ "$IS_WINDOWS" == "true" ]]; then
|
||||
cygpath -m "$1"
|
||||
else
|
||||
printf '%s' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
# On Windows, taskkill needs the Win32 pid. /proc/<msys_pid>/winpid disappears
|
||||
# once the bash wrapper exits, so we snapshot it right after backgrounding.
|
||||
# Guarded because `declare -gA` requires bash >= 4.2; macOS system bash is 3.2.
|
||||
if [[ "$IS_WINDOWS" == "true" ]]; then
|
||||
declare -gA WINPIDS=()
|
||||
fi
|
||||
track_winpid() {
|
||||
[[ "$IS_WINDOWS" == "true" ]] || return 0
|
||||
local pid=$1 wp
|
||||
wp=$(cat "/proc/$pid/winpid" 2>/dev/null) && WINPIDS[$pid]=$wp
|
||||
}
|
||||
|
||||
kill_tree() {
|
||||
local pid=$1 sig=${2:-TERM}
|
||||
if [[ "$IS_WINDOWS" == "true" ]]; then
|
||||
local wp=${WINPIDS[$pid]:-}
|
||||
[[ -z "$wp" ]] && wp=$(cat "/proc/$pid/winpid" 2>/dev/null)
|
||||
[[ -z "$wp" ]] && return 0
|
||||
if [[ "$sig" == "KILL" ]]; then
|
||||
taskkill //F //T //PID "$wp" >/dev/null 2>&1
|
||||
else
|
||||
taskkill //T //PID "$wp" >/dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
local children
|
||||
children=$(pgrep -P "$pid" 2>/dev/null)
|
||||
for child in $children; do
|
||||
kill_tree "$child" "$sig"
|
||||
done
|
||||
kill -"$sig" "$pid" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user