[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:
haikdc
2026-03-30 08:53:52 -07:00
parent 7f3df20970
commit 6deebbd052
10 changed files with 418 additions and 9 deletions
+13
View File
@@ -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"
}
}
+55
View File
@@ -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
}
}
}
]
}
+20 -6
View File
@@ -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
View File
@@ -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()
+3 -1
View File
@@ -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
+81
View File
@@ -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) |
+14
View File
@@ -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
}
+26
View File
@@ -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": []
}
}
+187
View File
@@ -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
View File
@@ -6,5 +6,8 @@
"frontend": {
"dev": 3000
},
"nineRouter": 20128
"nineRouter": {
"dev": 20129,
"prod": 20128
}
}