mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[Haik]: ckpt, nine router now runs on different ports for dev vs prod. Also made a custom linter to enforce my standards
This commit is contained in:
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"python.analysis.typeCheckingMode": "strict",
|
||||
"python.analysis.include": ["backend"],
|
||||
"python.analysis.exclude": [
|
||||
"backend/.venv",
|
||||
"backend/uv-bin",
|
||||
"backend/data",
|
||||
"backend/tests"
|
||||
],
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportMissingTypeStubs": "none"
|
||||
}
|
||||
}
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "structlint:watch",
|
||||
"type": "shell",
|
||||
"command": "python3",
|
||||
"args": ["${workspaceFolder}/linter/structlint.py", "--watch", "--root", "${workspaceFolder}"],
|
||||
"isBackground": true,
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "never",
|
||||
"panel": "dedicated",
|
||||
"close": true
|
||||
},
|
||||
"problemMatcher": {
|
||||
"owner": "structlint",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": {
|
||||
"regexp": "^(.+):(\\d+):(\\d+):\\s+(error|warning):\\s+(.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
},
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "^structlint: checking\\.\\.\\.$",
|
||||
"endsPattern": "^structlint: done\\."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "structlint:check",
|
||||
"type": "shell",
|
||||
"command": "python3",
|
||||
"args": ["${workspaceFolder}/linter/structlint.py", "--root", "${workspaceFolder}"],
|
||||
"problemMatcher": {
|
||||
"owner": "structlint",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": {
|
||||
"regexp": "^(.+):(\\d+):(\\d+):\\s+(error|warning):\\s+(.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"message": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
9Router is a free AI subscription proxy that lets users connect their
|
||||
Claude/ChatGPT/Gemini subscriptions to OpenSwarm without API keys.
|
||||
|
||||
It runs silently in the background on port 20128 and exposes an
|
||||
OpenAI-compatible API at localhost:20128/v1.
|
||||
It runs silently in the background and exposes an OpenAI-compatible API
|
||||
at localhost:<port>/v1. The port is read from ports.config.json (dev
|
||||
vs prod) so the packaged app and the dev server never collide.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -115,7 +116,12 @@ async def ensure_running():
|
||||
print(f"9Router: starting (production) on port {NINE_ROUTER_PORT}...", flush=True)
|
||||
cmd = [node, standalone_server]
|
||||
cwd = os.path.dirname(standalone_server)
|
||||
env = {**os.environ, "PORT": str(NINE_ROUTER_PORT), "NODE_ENV": "production"}
|
||||
env = {
|
||||
**os.environ,
|
||||
"PORT": str(NINE_ROUTER_PORT),
|
||||
"NEXT_PUBLIC_BASE_URL": NINE_ROUTER_URL,
|
||||
"NODE_ENV": "production",
|
||||
}
|
||||
if node == os.environ.get("OPENSWARM_ELECTRON_PATH"):
|
||||
env["ELECTRON_RUN_AS_NODE"] = "1"
|
||||
|
||||
@@ -135,7 +141,11 @@ async def ensure_running():
|
||||
print(f"9Router: starting (dev) on port {NINE_ROUTER_PORT}...", flush=True)
|
||||
cmd = [npx, "next", "dev", "--webpack", "-p", str(NINE_ROUTER_PORT)]
|
||||
cwd = _9router_dir
|
||||
env = {**os.environ, "PORT": str(NINE_ROUTER_PORT)}
|
||||
env = {
|
||||
**os.environ,
|
||||
"PORT": str(NINE_ROUTER_PORT),
|
||||
"NEXT_PUBLIC_BASE_URL": NINE_ROUTER_URL,
|
||||
}
|
||||
|
||||
else:
|
||||
npx = shutil.which("npx")
|
||||
@@ -143,9 +153,13 @@ async def ensure_running():
|
||||
print("9Router: npx not found and no bundled 9router directory", flush=True)
|
||||
return
|
||||
print(f"9Router: starting (npx) on port {NINE_ROUTER_PORT}...", flush=True)
|
||||
cmd = [npx, "9router"]
|
||||
cmd = [npx, "9router", "--port", str(NINE_ROUTER_PORT)]
|
||||
cwd = None
|
||||
env = {**os.environ, "PORT": str(NINE_ROUTER_PORT)}
|
||||
env = {
|
||||
**os.environ,
|
||||
"PORT": str(NINE_ROUTER_PORT),
|
||||
"NEXT_PUBLIC_BASE_URL": NINE_ROUTER_URL,
|
||||
}
|
||||
|
||||
try:
|
||||
_process = subprocess.Popen(
|
||||
|
||||
+15
-1
@@ -15,9 +15,23 @@ BACKEND_DEV_PORT: int = _cfg["backend"]["dev"]
|
||||
BACKEND_PROD_PORT_START: int = _cfg["backend"]["prod"]["start"]
|
||||
BACKEND_PROD_PORT_END: int = _cfg["backend"]["prod"]["end"]
|
||||
FRONTEND_DEV_PORT: int = _cfg["frontend"]["dev"]
|
||||
NINE_ROUTER_PORT: int = _cfg["nineRouter"]
|
||||
NINE_ROUTER_DEV_PORT: int = _cfg["nineRouter"]["dev"]
|
||||
NINE_ROUTER_PROD_PORT: int = _cfg["nineRouter"]["prod"]
|
||||
|
||||
|
||||
def _is_packaged() -> bool:
|
||||
return os.environ.get("OPENSWARM_PACKAGED") == "1"
|
||||
|
||||
|
||||
def get_backend_port() -> int:
|
||||
"""Return the active backend port (env override or dev default)."""
|
||||
return int(os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT)))
|
||||
|
||||
|
||||
def get_nine_router_port() -> int:
|
||||
"""Return the 9Router port for the current environment."""
|
||||
return NINE_ROUTER_PROD_PORT if _is_packaged() else NINE_ROUTER_DEV_PORT
|
||||
|
||||
|
||||
# Convenience alias — most callers just need the current port.
|
||||
NINE_ROUTER_PORT: int = get_nine_router_port()
|
||||
|
||||
@@ -8,4 +8,6 @@ pytest-asyncio==0.25.2
|
||||
typeguard==4.4.2
|
||||
Pillow
|
||||
posthog
|
||||
httpx>=0.27.0
|
||||
httpx>=0.27.0
|
||||
watchfiles
|
||||
pyright
|
||||
@@ -0,0 +1,81 @@
|
||||
# Structural Linter
|
||||
|
||||
A linter is a tool that automatically checks your code for problems. This one doesn't check for bugs — it enforces **structural rules** that keep the codebase organized and easy to navigate.
|
||||
|
||||
## What it checks
|
||||
|
||||
**1. File length** — Every source file must be under 250 lines.
|
||||
|
||||
Big files are hard to read, hard to review, and hard to maintain. If a file is getting long, it's a sign it should be split into smaller, more focused pieces.
|
||||
|
||||
**2. Folder size** — Every folder must contain fewer than 6 items (files or subfolders).
|
||||
|
||||
When a folder has dozens of files it becomes a junk drawer. Keeping folders small forces you to organize code into logical groups.
|
||||
|
||||
These rules apply to `.py`, `.ts`, `.tsx`, `.js`, and `.jsx` files. Non-code files (images, JSON data, configs, lock files, etc.) are ignored.
|
||||
|
||||
## How it runs
|
||||
|
||||
You don't need to do anything — it runs automatically.
|
||||
|
||||
When you open the project in Cursor/VS Code, a background task starts watching for file changes. Every time you save, it re-checks the codebase. Violations show up as errors in the **Problems panel** (`Cmd+Shift+M`) and as red badges on files in the sidebar, just like any other linter.
|
||||
|
||||
If you want to run it manually from the terminal:
|
||||
|
||||
```bash
|
||||
# one-shot check (exits with code 1 if violations exist)
|
||||
python3 linter/structlint.py --root .
|
||||
|
||||
# continuous watch mode
|
||||
python3 linter/structlint.py --watch --root .
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All config lives in `structlint.json` (this folder). Here's what each field does:
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"max-file-lines": 250, // files with >= this many lines trigger an error
|
||||
"max-folder-items": 6 // folders with >= this many items trigger an error
|
||||
},
|
||||
"include_extensions": [".py", ".ts", ".tsx", ".js", ".jsx"], // only these file types are line-counted
|
||||
"exclude": ["node_modules", ".venv", "..."], // directories to skip entirely
|
||||
"exceptions": {
|
||||
"max-file-lines": [], // glob patterns for files exempt from the line limit
|
||||
"max-folder-items": [] // glob patterns for folders exempt from the item limit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding exceptions
|
||||
|
||||
If a file or folder legitimately needs to exceed a limit, add a glob pattern to the `exceptions` list in `structlint.json`. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"exceptions": {
|
||||
"max-file-lines": [
|
||||
"backend/tests/test_analytics.py"
|
||||
],
|
||||
"max-folder-items": [
|
||||
"backend/apps/agents"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use wildcards: `"backend/tests/*"` exempts all files in the tests folder.
|
||||
|
||||
## Type checking
|
||||
|
||||
This folder also contains `pyrightconfig.json`, which configures strict type checking for the Python backend. This works through the **Pylance** extension in Cursor/VS Code — it shows type errors in real-time as you type, the same way TypeScript checks the frontend. No setup needed beyond having Pylance installed.
|
||||
|
||||
## Files in this folder
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `structlint.py` | The linter script |
|
||||
| `structlint.json` | Rules, exclusions, and exceptions |
|
||||
| `pyrightconfig.json` | Python type checking config (for CLI `pyright` usage) |
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include": ["../backend"],
|
||||
"exclude": [
|
||||
"../backend/.venv",
|
||||
"../backend/uv-bin",
|
||||
"../backend/data",
|
||||
"../backend/tests"
|
||||
],
|
||||
"typeCheckingMode": "strict",
|
||||
"pythonVersion": "3.11",
|
||||
"venvPath": "../backend",
|
||||
"venv": ".venv",
|
||||
"reportMissingTypeStubs": false
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"rules": {
|
||||
"max-file-lines": 250,
|
||||
"max-folder-items": 6
|
||||
},
|
||||
"include_extensions": [".py", ".ts", ".tsx", ".js", ".jsx"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
".venv",
|
||||
"dist",
|
||||
"build",
|
||||
"__pycache__",
|
||||
".git",
|
||||
".cursor",
|
||||
".vscode",
|
||||
"uv-bin",
|
||||
"data",
|
||||
"public",
|
||||
"readme_assets",
|
||||
"ASSISTANT_UI_MIGRATION"
|
||||
],
|
||||
"exceptions": {
|
||||
"max-file-lines": [],
|
||||
"max-folder-items": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural linter: enforces file line limits and folder item limits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
CONFIG_FILE = SCRIPT_DIR / "structlint.json"
|
||||
|
||||
|
||||
def load_config() -> dict[str, Any]:
|
||||
with open(CONFIG_FILE) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _matches_any(text: str, patterns: list[str]) -> bool:
|
||||
return any(fnmatch.fnmatch(text, p) for p in patterns)
|
||||
|
||||
|
||||
def is_excluded(path: Path, root: Path, excludes: list[str]) -> bool:
|
||||
rel = path.relative_to(root)
|
||||
for part in rel.parts:
|
||||
if _matches_any(part, excludes):
|
||||
return True
|
||||
return _matches_any(str(rel), excludes)
|
||||
|
||||
|
||||
def is_excepted(rel_path: str, rule: str, exceptions: dict[str, list[str]]) -> bool:
|
||||
return _matches_any(rel_path, exceptions.get(rule, []))
|
||||
|
||||
|
||||
def check_file_lines(
|
||||
filepath: Path, root: Path, max_lines: int,
|
||||
) -> tuple[str, int] | None:
|
||||
try:
|
||||
count = len(filepath.read_text(errors="ignore").splitlines())
|
||||
except OSError:
|
||||
return None
|
||||
if count >= max_lines:
|
||||
rel = filepath.relative_to(root)
|
||||
msg = (
|
||||
f"{rel}:1:1: error: File has {count} lines "
|
||||
f"(limit {max_lines}) [max-file-lines]"
|
||||
)
|
||||
return (msg, count)
|
||||
return None
|
||||
|
||||
|
||||
ANCHOR_FILES = ("__init__.py", "index.ts", "index.tsx", "index.js")
|
||||
|
||||
|
||||
def _find_anchor_file(dirpath: Path, root: Path) -> str:
|
||||
"""Find a real file inside the folder to attach the diagnostic to.
|
||||
|
||||
Prefers common entry-point files (__init__.py, index.ts, etc.) so the
|
||||
error shows up inline when you open that file. Falls back to the first
|
||||
file alphabetically, then the directory path itself.
|
||||
"""
|
||||
for name in ANCHOR_FILES:
|
||||
candidate = dirpath / name
|
||||
if candidate.exists():
|
||||
return str(candidate.relative_to(root))
|
||||
try:
|
||||
first = sorted(
|
||||
f for f in dirpath.iterdir()
|
||||
if f.is_file() and not f.name.startswith(".")
|
||||
)
|
||||
if first:
|
||||
return str(first[0].relative_to(root))
|
||||
except OSError:
|
||||
pass
|
||||
return str(dirpath.relative_to(root))
|
||||
|
||||
|
||||
def check_folder_items(
|
||||
dirpath: Path, root: Path, max_items: int, excludes: list[str],
|
||||
) -> tuple[str, int] | None:
|
||||
try:
|
||||
items = [
|
||||
i for i in dirpath.iterdir()
|
||||
if not i.name.startswith(".") and not _matches_any(i.name, excludes)
|
||||
]
|
||||
except OSError:
|
||||
return None
|
||||
count = len(items)
|
||||
if count >= max_items:
|
||||
anchor = _find_anchor_file(dirpath, root)
|
||||
rel = dirpath.relative_to(root)
|
||||
msg = (
|
||||
f"{anchor}:1:1: error: Folder '{rel}' has {count} items "
|
||||
f"(limit {max_items}) [max-folder-items]"
|
||||
)
|
||||
return (msg, count)
|
||||
return None
|
||||
|
||||
|
||||
def run_checks(root: Path) -> list[str]:
|
||||
config = load_config()
|
||||
rules: dict[str, int] = config["rules"]
|
||||
excludes: list[str] = config["exclude"]
|
||||
exceptions: dict[str, list[str]] = config["exceptions"]
|
||||
extensions: list[str] = config["include_extensions"]
|
||||
|
||||
max_lines: int = rules["max-file-lines"]
|
||||
max_items: int = rules["max-folder-items"]
|
||||
errors: list[str] = []
|
||||
|
||||
for dirpath_str, dirnames, filenames in os.walk(root):
|
||||
dp = Path(dirpath_str)
|
||||
|
||||
if is_excluded(dp, root, excludes):
|
||||
dirnames.clear()
|
||||
continue
|
||||
|
||||
rel_dir = str(dp.relative_to(root))
|
||||
if rel_dir != "." and not is_excepted(rel_dir, "max-folder-items", exceptions):
|
||||
result = check_folder_items(dp, root, max_items, excludes)
|
||||
if result:
|
||||
errors.append(result[0])
|
||||
|
||||
for fname in filenames:
|
||||
fp = dp / fname
|
||||
if fp.suffix not in extensions:
|
||||
continue
|
||||
if is_excluded(fp, root, excludes):
|
||||
continue
|
||||
rel_file = str(fp.relative_to(root))
|
||||
if not is_excepted(rel_file, "max-file-lines", exceptions):
|
||||
result = check_file_lines(fp, root, max_lines)
|
||||
if result:
|
||||
errors.append(result[0])
|
||||
|
||||
return sorted(errors)
|
||||
|
||||
|
||||
def print_results(errors: list[str]) -> None:
|
||||
print("structlint: checking...", flush=True)
|
||||
for err in errors:
|
||||
print(err, flush=True)
|
||||
count = len(errors)
|
||||
print(f"structlint: done. {count} error(s) found.", flush=True)
|
||||
|
||||
|
||||
def watch_loop(root: Path) -> None:
|
||||
from watchfiles import watch, DefaultFilter
|
||||
|
||||
print_results(run_checks(root))
|
||||
|
||||
class SourceFilter(DefaultFilter):
|
||||
allowed_extensions = (".py", ".ts", ".tsx", ".js", ".jsx")
|
||||
|
||||
def __call__(self, change: Any, path: str) -> bool:
|
||||
if not super().__call__(change, path):
|
||||
return False
|
||||
if Path(path).suffix in self.allowed_extensions:
|
||||
return True
|
||||
return Path(path).is_dir()
|
||||
|
||||
for _changes in watch(root, watch_filter=SourceFilter()):
|
||||
print_results(run_checks(root))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Structural linter")
|
||||
parser.add_argument("--watch", action="store_true", help="Watch for changes")
|
||||
parser.add_argument("--root", type=str, default=".", help="Root directory")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
|
||||
if args.watch:
|
||||
watch_loop(root)
|
||||
else:
|
||||
errors = run_checks(root)
|
||||
print_results(errors)
|
||||
sys.exit(1 if errors else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+4
-1
@@ -6,5 +6,8 @@
|
||||
"frontend": {
|
||||
"dev": 3000
|
||||
},
|
||||
"nineRouter": 20128
|
||||
"nineRouter": {
|
||||
"dev": 20129,
|
||||
"prod": 20128
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user