diff --git a/backend/.gitignore b/backend/.gitignore
index 6e519548..dfa780f7 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -15,4 +15,6 @@ apps/db/snips/*
!apps/db/snips/.gitkeep
# ignore everything in data/
-data/**
\ No newline at end of file
+data/**
+
+.venv/
\ No newline at end of file
diff --git a/backend/apps/app_builder/app_builder.py b/backend/apps/app_builder/app_builder.py
index b8614c00..8bbd3387 100644
--- a/backend/apps/app_builder/app_builder.py
+++ b/backend/apps/app_builder/app_builder.py
@@ -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")
diff --git a/backend/apps/app_builder/templates/templates.py b/backend/apps/app_builder/templates/templates.py
index 9b9c367f..e07022c8 100644
--- a/backend/apps/app_builder/templates/templates.py
+++ b/backend/apps/app_builder/templates/templates.py
@@ -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")
diff --git a/backend/apps/app_builder/utils/walk_directory.py b/backend/apps/app_builder/utils/walk_directory.py
index 85a5fc30..6c414c54 100644
--- a/backend/apps/app_builder/utils/walk_directory.py
+++ b/backend/apps/app_builder/utils/walk_directory.py
@@ -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
diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py
index eb643609..0d8e1d3d 100644
--- a/backend/apps/dashboards/dashboards.py
+++ b/backend/apps/dashboards/dashboards.py
@@ -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
diff --git a/backend/apps/settings/settings.py b/backend/apps/settings/settings.py
index edecf298..7e6b5382 100644
--- a/backend/apps/settings/settings.py
+++ b/backend/apps/settings/settings.py
@@ -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()}
diff --git a/backend/apps/skills/SkillStore/SkillStore.py b/backend/apps/skills/SkillStore/SkillStore.py
index 15eaaa1f..e26ce44b 100644
--- a/backend/apps/skills/SkillStore/SkillStore.py
+++ b/backend/apps/skills/SkillStore/SkillStore.py
@@ -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", ""),
diff --git a/backend/apps/skills/skills.py b/backend/apps/skills/skills.py
index a2af8b2f..6360b176 100644
--- a/backend/apps/skills/skills.py
+++ b/backend/apps/skills/skills.py
@@ -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)}
diff --git a/backend/apps/tools/discover_tools/utils/discover_mcp_tools_sse.py b/backend/apps/tools/discover_tools/utils/discover_mcp_tools_sse.py
index f6c8ea2a..536de461 100644
--- a/backend/apps/tools/discover_tools/utils/discover_mcp_tools_sse.py
+++ b/backend/apps/tools/discover_tools/utils/discover_mcp_tools_sse.py
@@ -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
diff --git a/backend/apps/tools/tools.py b/backend/apps/tools/tools.py
index a96879ad..d41d6281 100644
--- a/backend/apps/tools/tools.py
+++ b/backend/apps/tools/tools.py
@@ -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)
diff --git a/backend/core/db/PydanticStore.py b/backend/core/db/PydanticStore.py
index 6fb6145c..9128cc84 100644
--- a/backend/core/db/PydanticStore.py
+++ b/backend/core/db/PydanticStore.py
@@ -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
diff --git a/backend/core/shared_structs/agent/Message/prompt_utils.py b/backend/core/shared_structs/agent/Message/prompt_utils.py
index d964806d..a7bf0a9f 100644
--- a/backend/core/shared_structs/agent/Message/prompt_utils.py
+++ b/backend/core/shared_structs/agent/Message/prompt_utils.py
@@ -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'\n{content}\n'
diff --git a/backend/ports.py b/backend/ports.py
index cc690a6d..731ce2ec 100644
--- a/backend/ports.py
+++ b/backend/ports.py
@@ -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"]
diff --git a/backend/run.sh b/backend/run.sh
index bab8e2ac..d556bda4 100755
--- a/backend/run.sh
+++ b/backend/run.sh
@@ -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'
\ No newline at end of file
+ --reload-exclude '*.pyc'
diff --git a/frontend/run.sh b/frontend/run.sh
index 0fa200ab..4bb6bb85 100755
--- a/frontend/run.sh
+++ b/frontend/run.sh
@@ -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 -
\ No newline at end of file
+cd -
diff --git a/linter/print_errors.sh b/linter/print_errors.sh
index ac6c8d89..2f1dc36f 100755
--- a/linter/print_errors.sh
+++ b/linter/print_errors.sh
@@ -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
diff --git a/run/local.sh b/run/local.sh
index c8aec1c1..4baf465a 100755
--- a/run/local.sh
+++ b/run/local.sh
@@ -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-/ 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}"
diff --git a/run/utils/platform.sh b/run/utils/platform.sh
new file mode 100644
index 00000000..776a5443
--- /dev/null
+++ b/run/utils/platform.sh
@@ -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 Strip CRLF from a file (BSD/GNU sed safe)
+# py_path Convert /c/... to C:/... on Windows
+# track_winpid Snapshot Win32 pid for later taskkill
+# kill_tree [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//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
+}